From 6aaba2bf3d75aad015731f74718dc7ee27074dd6 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 12 Aug 2024 13:19:08 +1000 Subject: [PATCH 001/178] Drop Python 3.8 and 3.9 --- .gitlab-ci.yml | 52 ++--------------------------------------- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 +--- tox.ini | 4 +--- 4 files changed, 5 insertions(+), 57 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 41582a43..bdfbc79a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -51,30 +51,6 @@ secret_detection: stage: gitlab before_script: [] -test-3.8-core: - <<: *only-default - image: python:3.8-bullseye - script: - - tox -e py38-core - artifacts: - when: always - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - -test-3.9-core: - <<: *only-default - image: python:3.9-bullseye - script: - - tox -e py39-core - artifacts: - when: always - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - test-3.10-core: <<: *only-default image: python:3.10-bullseye @@ -101,7 +77,7 @@ test-3.11-core: test-3.12-core: <<: *only-default - image: python:3.12-rc-bullseye + image: python:3.12-bullseye script: - tox -e py312-core artifacts: @@ -111,30 +87,6 @@ test-3.12-core: coverage_format: cobertura path: coverage.xml -test-3.8-all: - <<: *only-default - image: python:3.8-bullseye - script: - - tox -e py38-all - artifacts: - when: always - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - -test-3.9-all: - <<: *only-default - image: python:3.9-bullseye - script: - - tox -e py39-all - artifacts: - when: always - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - test-3.10-all: <<: *only-default image: python:3.10-bullseye @@ -162,7 +114,7 @@ test-3.11-all: test-3.12-all: <<: *only-default - image: python:3.12-rc-bullseye + image: python:3.12-bullseye script: - tox -e py312-all artifacts: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04c74b56..01bb6c7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: rev: v3.15.2 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py310-plus] - repo: https://github.com/adamchainz/django-upgrade rev: 1.17.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 3045997f..83cec687 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = { file = "LICENSE" } authors = [ { name = "Alliance Auth", email = "adarnof@gmail.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.10" classifiers = [ "Environment :: Web Environment", "Framework :: Django", @@ -25,8 +25,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tox.ini b/tox.ini index 6c90339e..0930e784 100644 --- a/tox.ini +++ b/tox.ini @@ -2,15 +2,13 @@ isolated_build = true skipsdist = true usedevelop = true -envlist = py{38,39,310,311,312}-{all,core}, docs +envlist = py{310,311,312}-{all,core}, docs [testenv] setenv = all: DJANGO_SETTINGS_MODULE = tests.settings_all core: DJANGO_SETTINGS_MODULE = tests.settings_core basepython = - py38: python3.8 - py39: python3.9 py310: python3.10 py311: python3.11 py312: python3.12 From 046473def17d4816702dea17a66d0753312364bc Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 12 Aug 2024 13:19:57 +1000 Subject: [PATCH 002/178] use 3.12 in builds --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bdfbc79a..b4d5841c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -126,7 +126,7 @@ test-3.12-all: build-test: stage: test - image: python:3.11-bullseye + image: python:3.12-bullseye before_script: - python -m pip install --upgrade pip @@ -145,13 +145,13 @@ build-test: test-docs: <<: *only-default - image: python:3.11-bullseye + image: python:3.12-bullseye script: - tox -e docs deploy_production: stage: deploy - image: python:3.11-bullseye + image: python:3.12-bullseye before_script: - python -m pip install --upgrade pip From 5ee34fcb2d92d4200453087445ffdb3a1074760f Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 12 Aug 2024 13:20:11 +1000 Subject: [PATCH 003/178] bring docker major up --- .gitlab-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b4d5841c..3a63fe09 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -167,10 +167,10 @@ deploy_production: build-image: before_script: [] - image: docker:24.0 + image: docker:27.0 stage: docker services: - - docker:24.0-dind + - docker:27-dind script: | CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA @@ -191,10 +191,10 @@ build-image: build-image-dev: before_script: [] - image: docker:24.0 + image: docker:27 stage: docker services: - - docker:24.0-dind + - docker:27-dind script: | CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA @@ -212,10 +212,10 @@ build-image-dev: build-image-mr: before_script: [] - image: docker:24.0 + image: docker:27 stage: docker services: - - docker:24.0-dind + - docker:27-dind script: | CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA From 84484cebcb8f56b0dc6b45e0fb3b726910f602d3 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 12 Aug 2024 13:20:27 +1000 Subject: [PATCH 004/178] move to django 5.1.x --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 83cec687..41b1f34f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "beautifulsoup4", "celery<6,>=5.2", "celery-once>=3.0.1", - "django<5,>=4.2", + "django<5.2,>=5.1", "django-bootstrap-form", "django-bootstrap5>=23.3", "django-celery-beat>=2.3", From c88521af88aa1ddacb1210ebb3d60b21179ae78a Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 22 Aug 2024 12:54:31 +1000 Subject: [PATCH 005/178] django-celery-beat >=2.7.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b0f25b08..fcc28c88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "django<5.2,>=5.1", "django-bootstrap-form", "django-bootstrap5>=23.3", - "django-celery-beat>=2.3", + "django-celery-beat>=2.7", "django-esi>=5", "django-redis>=5.2", "django-registration<3.4,>=3.3", From d50f13528bc832eaa00d2e8f2dae3390f0a66db7 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 22 Aug 2024 12:58:54 +1000 Subject: [PATCH 006/178] add python 3.13 RC to tox allow fail --- .gitlab-ci.yml | 26 ++++++++++++++++++++++++++ tox.ini | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be852702..cc8a1370 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -87,6 +87,19 @@ test-3.12-core: coverage_format: cobertura path: coverage.xml +test-3.13-core: + <<: *only-default + image: python:3.13-rc-bookworm⁠ + script: + - tox -e py313-core + artifacts: + when: always + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + allow_failure: true + test-3.10-all: <<: *only-default image: python:3.10-bookworm @@ -124,6 +137,19 @@ test-3.12-all: coverage_format: cobertura path: coverage.xml +test-3.13-all: + <<: *only-default + image: python:3.13-rc-bookworm⁠ + script: + - tox -e py313-all + artifacts: + when: always + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + allow_failure: true + build-test: stage: test image: python:3.12-bookworm diff --git a/tox.ini b/tox.ini index 0930e784..d8f96955 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ isolated_build = true skipsdist = true usedevelop = true -envlist = py{310,311,312}-{all,core}, docs +envlist = py{310,311,312,313}-{all,core}, docs [testenv] setenv = @@ -12,6 +12,7 @@ basepython = py310: python3.10 py311: python3.11 py312: python3.12 + py313: python3.13 deps= coverage install_command = pip install -e ".[test]" -U {opts} {packages} From bbcb94021e67988b27452f98a5b03d711b27e166 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Fri, 23 Aug 2024 13:55:25 +1000 Subject: [PATCH 007/178] run pyupgrade --- .../authentication/core/celery_workers.py | 6 ++-- allianceauth/authentication/decorators.py | 5 +-- .../task_statistics/counters.py | 2 +- .../task_statistics/event_series.py | 6 ++-- allianceauth/checks.py | 28 ++++++++--------- allianceauth/eveonline/models.py | 4 +-- allianceauth/framework/api/evecharacter.py | 2 +- allianceauth/framework/api/user.py | 2 +- allianceauth/groupmanagement/models.py | 2 +- allianceauth/hrapplications/managers.py | 2 +- allianceauth/menu/admin.py | 6 ++-- allianceauth/menu/core/menu_item_hooks.py | 4 +-- allianceauth/menu/hooks.py | 4 +-- .../menu/templatetags/menu_menu_items.py | 14 ++++----- .../templatetags/test_menu_menu_items.py | 4 +-- allianceauth/services/hooks.py | 5 +-- allianceauth/services/modules/discord/api.py | 2 +- allianceauth/services/modules/discord/core.py | 8 ++--- .../modules/discord/discord_client/client.py | 31 ++++++++++--------- .../modules/discord/discord_client/helpers.py | 11 ++++--- .../modules/discord/discord_client/models.py | 4 +-- .../services/modules/discord/models.py | 6 ++-- allianceauth/services/modules/smf/manager.py | 2 +- allianceauth/tests/auth_utils.py | 2 +- allianceauth/theme/hooks.py | 12 +++---- allianceauth/urls.py | 7 +++-- allianceauth/utils/counters.py | 4 +-- 27 files changed, 95 insertions(+), 90 deletions(-) diff --git a/allianceauth/authentication/core/celery_workers.py b/allianceauth/authentication/core/celery_workers.py index 6da60924..ceb1f40d 100644 --- a/allianceauth/authentication/core/celery_workers.py +++ b/allianceauth/authentication/core/celery_workers.py @@ -12,7 +12,7 @@ from django.conf import settings logger = logging.getLogger(__name__) -def active_tasks_count() -> Optional[int]: +def active_tasks_count() -> int | None: """Return count of currently active tasks or None if celery workers are not online. """ @@ -20,7 +20,7 @@ def active_tasks_count() -> Optional[int]: return _tasks_count(inspect.active()) -def _tasks_count(data: dict) -> Optional[int]: +def _tasks_count(data: dict) -> int | None: """Return count of tasks in data from celery inspect API.""" try: tasks = itertools.chain(*data.values()) @@ -29,7 +29,7 @@ def _tasks_count(data: dict) -> Optional[int]: return len(list(tasks)) -def queued_tasks_count() -> Optional[int]: +def queued_tasks_count() -> int | None: """Return count of queued tasks. Return None if there was an error.""" try: with current_app.connection_or_acquire() as conn: diff --git a/allianceauth/authentication/decorators.py b/allianceauth/authentication/decorators.py index d9d93faa..81403615 100644 --- a/allianceauth/authentication/decorators.py +++ b/allianceauth/authentication/decorators.py @@ -2,7 +2,8 @@ from django.urls import include from django.contrib.auth.decorators import user_passes_test from django.core.exceptions import PermissionDenied from functools import wraps -from typing import Callable, Iterable, Optional +from typing import Optional +from collections.abc import Callable, Iterable from django.urls import include from django.contrib import messages @@ -17,7 +18,7 @@ def user_has_main_character(user): def decorate_url_patterns( - urls, decorator: Callable, excluded_views: Optional[Iterable] = None + urls, decorator: Callable, excluded_views: Iterable | None = None ): """Decorate views given in url patterns except when they are explicitly excluded. diff --git a/allianceauth/authentication/task_statistics/counters.py b/allianceauth/authentication/task_statistics/counters.py index bdb6d034..6d1f6175 100644 --- a/allianceauth/authentication/task_statistics/counters.py +++ b/allianceauth/authentication/task_statistics/counters.py @@ -16,7 +16,7 @@ class _TaskCounts(NamedTuple): retried: int failed: int total: int - earliest_task: Optional[dt.datetime] + earliest_task: dt.datetime | None hours: int diff --git a/allianceauth/authentication/task_statistics/event_series.py b/allianceauth/authentication/task_statistics/event_series.py index 9ac1f15d..3eb96abe 100644 --- a/allianceauth/authentication/task_statistics/event_series.py +++ b/allianceauth/authentication/task_statistics/event_series.py @@ -17,7 +17,7 @@ class EventSeries: _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" - def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None: + def __init__(self, key_id: str, redis: Redis | None = None) -> None: self._redis = get_redis_client_or_stub() if not redis else redis self._key_id = str(key_id) self.clear() @@ -46,7 +46,7 @@ class EventSeries: my_id = self._redis.incr(self._key_counter) self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()}) - def all(self) -> List[dt.datetime]: + def all(self) -> list[dt.datetime]: """List of all known events.""" return [ event[1] @@ -75,7 +75,7 @@ class EventSeries: maximum = "+inf" if not latest else latest.timestamp() return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum) - def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]: + def first_event(self, earliest: dt.datetime = None) -> dt.datetime | None: """Date/Time of first event. Returns `None` if series has no events. Args: diff --git a/allianceauth/checks.py b/allianceauth/checks.py index 19b0adec..657c7313 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -15,8 +15,8 @@ B = Configuration @register() -def django_settings(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def django_settings(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] if hasattr(settings, "SITE_URL"): if settings.SITE_URL[-1] == "/": errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005")) @@ -33,8 +33,8 @@ def django_settings(app_configs, **kwargs) -> List[CheckMessage]: @register() -def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] try: redis_version = Pep440Version(get_redis_client().info()['redis_version']) except InvalidVersion: @@ -54,8 +54,8 @@ def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]: @register() -def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] for connection in db.connections.all(): if connection.vendor == "mysql": @@ -82,8 +82,8 @@ def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]: @register() -def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] for connection in db.connections.all(): if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB @@ -121,8 +121,8 @@ def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]: @register() -def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def system_package_sqlite(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] for connection in db.connections.all(): if connection.vendor == "sqlite": try: @@ -136,8 +136,8 @@ def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]: @register() -def sql_settings(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def sql_settings(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] for connection in db.connections.all(): if connection.vendor == "mysql": try: @@ -159,8 +159,8 @@ def sql_settings(app_configs, **kwargs) -> List[CheckMessage]: @register() -def celery_settings(app_configs, **kwargs) -> List[CheckMessage]: - errors: List[CheckMessage] = [] +def celery_settings(app_configs, **kwargs) -> list[CheckMessage]: + errors: list[CheckMessage] = [] try: if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}: diff --git a/allianceauth/eveonline/models.py b/allianceauth/eveonline/models.py index fb279c35..85993cbd 100644 --- a/allianceauth/eveonline/models.py +++ b/allianceauth/eveonline/models.py @@ -235,7 +235,7 @@ class EveCharacter(models.Model): return self.corporation_id == DOOMHEIM_CORPORATION_ID @property - def alliance(self) -> Union[EveAllianceInfo, None]: + def alliance(self) -> EveAllianceInfo | None: """ Pseudo foreign key from alliance_id to EveAllianceInfo :raises: EveAllianceInfo.DoesNotExist @@ -255,7 +255,7 @@ class EveCharacter(models.Model): return EveCorporationInfo.objects.get(corporation_id=self.corporation_id) @property - def faction(self) -> Union[EveFactionInfo, None]: + def faction(self) -> EveFactionInfo | None: """ Pseudo foreign key from faction_id to EveFactionInfo :raises: EveFactionInfo.DoesNotExist diff --git a/allianceauth/framework/api/evecharacter.py b/allianceauth/framework/api/evecharacter.py index 0d17707e..d6a77f05 100644 --- a/allianceauth/framework/api/evecharacter.py +++ b/allianceauth/framework/api/evecharacter.py @@ -13,7 +13,7 @@ from allianceauth.framework.api.user import get_sentinel_user def get_main_character_from_evecharacter( character: EveCharacter, -) -> Optional[EveCharacter]: +) -> EveCharacter | None: """ Get the main character for a given EveCharacter or None when no main character is set diff --git a/allianceauth/framework/api/user.py b/allianceauth/framework/api/user.py index 51b314ae..e1597e72 100644 --- a/allianceauth/framework/api/user.py +++ b/allianceauth/framework/api/user.py @@ -34,7 +34,7 @@ def get_all_characters_from_user(user: User) -> list: return characters -def get_main_character_from_user(user: User) -> Optional[EveCharacter]: +def get_main_character_from_user(user: User) -> EveCharacter | None: """ Get the main character from a user diff --git a/allianceauth/groupmanagement/models.py b/allianceauth/groupmanagement/models.py index 8491f08f..19e15c72 100644 --- a/allianceauth/groupmanagement/models.py +++ b/allianceauth/groupmanagement/models.py @@ -181,7 +181,7 @@ class AuthGroup(models.Model): def __str__(self): return self.group.name - def group_request_approvers(self) -> Set[User]: + def group_request_approvers(self) -> set[User]: """Return all users who can approve a group request.""" return set( self.group_leaders.all() diff --git a/allianceauth/hrapplications/managers.py b/allianceauth/hrapplications/managers.py index 30ee5e74..7c518520 100644 --- a/allianceauth/hrapplications/managers.py +++ b/allianceauth/hrapplications/managers.py @@ -5,7 +5,7 @@ from typing import Optional class ApplicationManager(models.Manager): - def pending_requests_count_for_user(self, user: User) -> Optional[int]: + def pending_requests_count_for_user(self, user: User) -> int | None: """Returns the number of pending group requests for the given user""" if user.is_superuser: return self.filter(approved__isnull=True).count() diff --git a/allianceauth/menu/admin.py b/allianceauth/menu/admin.py index 49f590be..7df1c250 100644 --- a/allianceauth/menu/admin.py +++ b/allianceauth/menu/admin.py @@ -35,12 +35,12 @@ class MenuItemAdmin(admin.ModelAdmin): ] ordering = ["parent", "order", "text"] - def get_form(self, request: HttpRequest, obj: Optional[MenuItem] = None, **kwargs): + def get_form(self, request: HttpRequest, obj: MenuItem | None = None, **kwargs): kwargs["form"] = self._choose_form(request, obj) return super().get_form(request, obj, **kwargs) @classmethod - def _choose_form(cls, request: HttpRequest, obj: Optional[MenuItem]): + def _choose_form(cls, request: HttpRequest, obj: MenuItem | None): """Return the form for the current menu item type.""" if obj: # change if obj.hook_hash: @@ -104,7 +104,7 @@ class MenuItemAdmin(admin.ModelAdmin): @staticmethod def _type_from_request( request: HttpRequest, default=None - ) -> Optional[MenuItemType]: + ) -> MenuItemType | None: try: return MenuItemType(request.GET.get("type")) except ValueError: diff --git a/allianceauth/menu/core/menu_item_hooks.py b/allianceauth/menu/core/menu_item_hooks.py index 4307b3a1..2114569e 100644 --- a/allianceauth/menu/core/menu_item_hooks.py +++ b/allianceauth/menu/core/menu_item_hooks.py @@ -14,8 +14,8 @@ class MenuItemHookCustom(MenuItemHook): text: str, classes: str, url_name: str, - order: Optional[int] = None, - navactive: Optional[List[str]] = None, + order: int | None = None, + navactive: list[str] | None = None, ): super().__init__(text, classes, url_name, order, navactive) self.url = "" diff --git a/allianceauth/menu/hooks.py b/allianceauth/menu/hooks.py index d751c8e5..9341415c 100644 --- a/allianceauth/menu/hooks.py +++ b/allianceauth/menu/hooks.py @@ -33,8 +33,8 @@ class MenuItemHook: text: str, classes: str, url_name: str, - order: Optional[int] = None, - navactive: Optional[List[str]] = None, + order: int | None = None, + navactive: list[str] | None = None, ): self.text = text self.classes = classes diff --git a/allianceauth/menu/templatetags/menu_menu_items.py b/allianceauth/menu/templatetags/menu_menu_items.py index fbbd22a1..2049e56d 100644 --- a/allianceauth/menu/templatetags/menu_menu_items.py +++ b/allianceauth/menu/templatetags/menu_menu_items.py @@ -56,8 +56,8 @@ class RenderedMenuItem: menu_item: MenuItem - children: List["RenderedMenuItem"] = field(default_factory=list) - count: Optional[int] = None + children: list["RenderedMenuItem"] = field(default_factory=list) + count: int | None = None html: str = "" html_id: str = "" @@ -78,7 +78,7 @@ class RenderedMenuItem: self.html_id = hook_obj.html_id -def render_menu(request: HttpRequest) -> List[RenderedMenuItem]: +def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: """Return the rendered side menu for including in a template. This function is creating BS5 style menus. @@ -88,7 +88,7 @@ def render_menu(request: HttpRequest) -> List[RenderedMenuItem]: # Menu items needs to be rendered with the new BS5 template bs5_template = "menu/menu-item-bs5.html" - rendered_items: Dict[int, RenderedMenuItem] = {} + rendered_items: dict[int, RenderedMenuItem] = {} menu_items: QuerySet[MenuItem] = MenuItem.objects.order_by( "parent", "order", "text" ) @@ -131,7 +131,7 @@ def render_menu(request: HttpRequest) -> List[RenderedMenuItem]: return list(rendered_items.values()) -def _gather_menu_items_from_hooks() -> Dict[str, MenuItemHook]: +def _gather_menu_items_from_hooks() -> dict[str, MenuItemHook]: hook_items = {} for hook in get_hooks("menu_item_hook"): f = hook() @@ -161,14 +161,14 @@ def _render_link_item( def _render_folder_items( - request: HttpRequest, rendered_items: Dict[int, RenderedMenuItem], new_template: str + request: HttpRequest, rendered_items: dict[int, RenderedMenuItem], new_template: str ): for item in rendered_items.values(): if item.menu_item.is_folder: item.update_html(request=request, template=new_template) -def _remove_empty_folders(rendered_items: Dict[int, RenderedMenuItem]): +def _remove_empty_folders(rendered_items: dict[int, RenderedMenuItem]): ids_to_remove = [] for item_id, item in rendered_items.items(): if item.is_folder and not item.children: diff --git a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py index 3e3e829a..ff48a49a 100644 --- a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py +++ b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py @@ -347,9 +347,9 @@ class TestRenderedMenuItem(TestCase): class _ParsedMenuItem(NamedTuple): - classes: List[str] + classes: list[str] text: str - count: Optional[int] + count: int | None def parse_html(obj: RenderedMenuItem) -> _ParsedMenuItem: diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 084d8e49..458f9266 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -1,6 +1,7 @@ from string import Formatter from django.urls import include, re_path -from typing import Iterable, Optional +from typing import Optional +from collections.abc import Iterable from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -175,7 +176,7 @@ class UrlHook: urls, namespace: str, base_url: str, - excluded_views : Optional[Iterable[str]] = None + excluded_views : Iterable[str] | None = None ): self.include_pattern = re_path(base_url, include(urls, namespace=namespace)) self.excluded_views = set(excluded_views or []) diff --git a/allianceauth/services/modules/discord/api.py b/allianceauth/services/modules/discord/api.py index fbbf30d8..5805d730 100644 --- a/allianceauth/services/modules/discord/api.py +++ b/allianceauth/services/modules/discord/api.py @@ -28,7 +28,7 @@ from .models import DiscordUser # noqa __all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"] -def discord_guild_id() -> Optional[int]: +def discord_guild_id() -> int | None: """Guild ID of configured Discord server. Returns: diff --git a/allianceauth/services/modules/discord/core.py b/allianceauth/services/modules/discord/core.py index 27acb3c7..7b1f76e7 100644 --- a/allianceauth/services/modules/discord/core.py +++ b/allianceauth/services/modules/discord/core.py @@ -36,7 +36,7 @@ def calculate_roles_for_user( client: DiscordClient, discord_uid: int, state_name: str = None, -) -> Tuple[RolesSet, Optional[bool]]: +) -> tuple[RolesSet, bool | None]: """Calculate current Discord roles for an Auth user. Takes into account reserved groups and existing managed roles (e.g. nitro). @@ -68,7 +68,7 @@ def calculate_roles_for_user( return roles_calculated.union(roles_persistent), True -def _user_group_names(user: User, state_name: str = None) -> List[str]: +def _user_group_names(user: User, state_name: str = None) -> list[str]: """Names of groups and state the given user is a member of.""" if not state_name: state_name = user.profile.state.name @@ -77,7 +77,7 @@ def _user_group_names(user: User, state_name: str = None) -> List[str]: return group_names -def user_formatted_nick(user: User) -> Optional[str]: +def user_formatted_nick(user: User) -> str | None: """Name of the given user's main character with name formatting applied. Returns: @@ -90,7 +90,7 @@ def user_formatted_nick(user: User) -> Optional[str]: return None -def group_to_role(group: Group) -> Optional[Role]: +def group_to_role(group: Group) -> Role | None: """Fetch the Discord role matching the given Django group by name. Returns: diff --git a/allianceauth/services/modules/discord/discord_client/client.py b/allianceauth/services/modules/discord/discord_client/client.py index 048088e3..73710270 100644 --- a/allianceauth/services/modules/discord/discord_client/client.py +++ b/allianceauth/services/modules/discord/discord_client/client.py @@ -6,7 +6,8 @@ from enum import IntEnum from hashlib import md5 from http import HTTPStatus from time import sleep -from typing import Iterable, List, Optional, Set, Tuple +from typing import List, Optional, Set, Tuple +from collections.abc import Iterable from urllib.parse import urljoin from uuid import uuid1 @@ -233,7 +234,7 @@ class DiscordClient: # guild roles - def guild_roles(self, guild_id: int, use_cache: bool = True) -> Set[Role]: + def guild_roles(self, guild_id: int, use_cache: bool = True) -> set[Role]: """Fetch all roles for this guild. Args: @@ -268,7 +269,7 @@ class DiscordClient: def create_guild_role( self, guild_id: int, role_name: str, **kwargs - ) -> Optional[Role]: + ) -> Role | None: """Create a new guild role with the given name. See official documentation for additional optional parameters. @@ -318,7 +319,7 @@ class DiscordClient: gen_key = cls._generate_hash(f'{guild_id}') return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}' - def match_role_from_name(self, guild_id: int, role_name: str) -> Optional[Role]: + def match_role_from_name(self, guild_id: int, role_name: str) -> Role | None: """Fetch Discord role matching the given name (cached). Args: @@ -333,7 +334,7 @@ class DiscordClient: def match_or_create_roles_from_names( self, guild_id: int, role_names: Iterable[str] - ) -> List[Tuple[Role, bool]]: + ) -> list[tuple[Role, bool]]: """Fetch or create Discord roles matching the given names (cached). Will try to match with existing roles names @@ -361,7 +362,7 @@ class DiscordClient: def match_or_create_role_from_name( self, guild_id: int, role_name: str, guild_roles: RolesSet = None - ) -> Tuple[Role, bool]: + ) -> tuple[Role, bool]: """Fetch or create Discord role matching the given name. Will try to match with existing roles names @@ -418,7 +419,7 @@ class DiscordClient: access_token: str, role_ids: list = None, nick: str = None - ) -> Optional[bool]: + ) -> bool | None: """Adds a user to the guild. Returns: @@ -442,7 +443,7 @@ class DiscordClient: return None return False - def guild_member(self, guild_id: int, user_id: int) -> Optional[GuildMember]: + def guild_member(self, guild_id: int, user_id: int) -> GuildMember | None: """Fetch info for a guild member. Args: @@ -461,8 +462,8 @@ class DiscordClient: return GuildMember.from_dict(r.json()) def modify_guild_member( - self, guild_id: int, user_id: int, role_ids: List[int] = None, nick: str = None - ) -> Optional[bool]: + self, guild_id: int, user_id: int, role_ids: list[int] = None, nick: str = None + ) -> bool | None: """Set properties of a guild member. Args: @@ -501,7 +502,7 @@ class DiscordClient: return True return False - def remove_guild_member(self, guild_id: int, user_id: int) -> Optional[bool]: + def remove_guild_member(self, guild_id: int, user_id: int) -> bool | None: """Remove a member from a guild. Args: @@ -529,7 +530,7 @@ class DiscordClient: def add_guild_member_role( self, guild_id: int, user_id: int, role_id: int - ) -> Optional[bool]: + ) -> bool | None: """Adds a role to a guild member Returns: @@ -549,7 +550,7 @@ class DiscordClient: def remove_guild_member_role( self, guild_id: int, user_id: int, role_id: int - ) -> Optional[bool]: + ) -> bool | None: """Remove a role to a guild member Args: @@ -572,7 +573,7 @@ class DiscordClient: return True return False - def guild_member_roles(self, guild_id: int, user_id: int) -> Optional[RolesSet]: + def guild_member_roles(self, guild_id: int, user_id: int) -> RolesSet | None: """Fetch the current guild roles of a guild member. Args: @@ -821,6 +822,6 @@ class DiscordClient: return md5(key.encode('utf-8')).hexdigest() @staticmethod - def _sanitize_role_ids(role_ids: Iterable[int]) -> List[int]: + def _sanitize_role_ids(role_ids: Iterable[int]) -> list[int]: """Sanitize a list of role IDs, i.e. make sure its a list of unique integers.""" return [int(role_id) for role_id in set(role_ids)] diff --git a/allianceauth/services/modules/discord/discord_client/helpers.py b/allianceauth/services/modules/discord/discord_client/helpers.py index c8453040..2932e3c5 100644 --- a/allianceauth/services/modules/discord/discord_client/helpers.py +++ b/allianceauth/services/modules/discord/discord_client/helpers.py @@ -1,5 +1,6 @@ from copy import copy -from typing import Iterable, List, Optional, Set, Tuple +from typing import List, Optional, Set, Tuple +from collections.abc import Iterable from .models import Role @@ -50,7 +51,7 @@ class RolesSet: def __len__(self): return len(self._roles.keys()) - def has_roles(self, role_ids: Set[int]) -> bool: + def has_roles(self, role_ids: set[int]) -> bool: """True if this objects contains all roles defined by given role_ids incl. managed roles. """ @@ -58,7 +59,7 @@ class RolesSet: all_role_ids = self._roles.keys() return role_ids.issubset(all_role_ids) - def ids(self) -> Set[int]: + def ids(self) -> set[int]: """Set of all role IDs.""" return set(self._roles.keys()) @@ -114,7 +115,7 @@ class RolesSet: new_ids = self.ids().difference(other.ids()) return self.subset(role_ids=new_ids) - def role_by_name(self, role_name: str) -> Optional[Role]: + def role_by_name(self, role_name: str) -> Role | None: """Role if one with matching name is found else None.""" role_name = Role.sanitize_name(role_name) if role_name in self._roles_by_name: @@ -123,7 +124,7 @@ class RolesSet: @classmethod def create_from_matched_roles( - cls, matched_roles: List[Tuple[Role, bool]] + cls, matched_roles: list[tuple[Role, bool]] ) -> "RolesSet": """Create new instance from the given list of matches roles. diff --git a/allianceauth/services/modules/discord/discord_client/models.py b/allianceauth/services/modules/discord/discord_client/models.py index f38ece8f..2f8f230a 100644 --- a/allianceauth/services/modules/discord/discord_client/models.py +++ b/allianceauth/services/modules/discord/discord_client/models.py @@ -69,7 +69,7 @@ class Guild: id: int name: str - roles: FrozenSet[Role] + roles: frozenset[Role] def __post_init__(self): object.__setattr__(self, "id", int(self.id)) @@ -95,7 +95,7 @@ class GuildMember: _NICK_MAX_CHARS = 32 - roles: FrozenSet[int] + roles: frozenset[int] nick: str = None user: User = None diff --git a/allianceauth/services/modules/discord/models.py b/allianceauth/services/modules/discord/models.py index 1f9c084f..4d74b247 100644 --- a/allianceauth/services/modules/discord/models.py +++ b/allianceauth/services/modules/discord/models.py @@ -98,7 +98,7 @@ class DiscordUser(models.Model): logger.warning('Failed to update nickname for %s', self.user) return success - def update_groups(self, state_name: str = None) -> Optional[bool]: + def update_groups(self, state_name: str = None) -> bool | None: """update groups for a user based on his current group memberships. Will add or remove roles of a user as needed. @@ -134,7 +134,7 @@ class DiscordUser(models.Model): logger.info('No need to update roles for user %s', self.user) return True - def update_username(self) -> Optional[bool]: + def update_username(self) -> bool | None: """Updates the username incl. the discriminator from the Discord server and saves it @@ -159,7 +159,7 @@ class DiscordUser(models.Model): notify_user: bool = False, is_rate_limited: bool = True, handle_api_exceptions: bool = False - ) -> Optional[bool]: + ) -> bool | None: """Deletes the Discount user both on the server and locally Params: diff --git a/allianceauth/services/modules/smf/manager.py b/allianceauth/services/modules/smf/manager.py index 9a3d7049..d0f52617 100644 --- a/allianceauth/services/modules/smf/manager.py +++ b/allianceauth/services/modules/smf/manager.py @@ -181,7 +181,7 @@ class SmfManager: return out @classmethod - def add_user(cls, username, email_address, groups, main_character: EveCharacter) -> Tuple: + def add_user(cls, username, email_address, groups, main_character: EveCharacter) -> tuple: """ Add a user to SMF :param username: diff --git a/allianceauth/tests/auth_utils.py b/allianceauth/tests/auth_utils.py index 7f759b84..06bfb314 100644 --- a/allianceauth/tests/auth_utils.py +++ b/allianceauth/tests/auth_utils.py @@ -262,7 +262,7 @@ class AuthUtils: @classmethod def add_permissions_to_user_by_name( - cls, perms: List[str], user: User, disconnect_signals: bool = True + cls, perms: list[str], user: User, disconnect_signals: bool = True ) -> User: """Add permissions given by name to a user diff --git a/allianceauth/theme/hooks.py b/allianceauth/theme/hooks.py index 7a618676..bebb5f2b 100644 --- a/allianceauth/theme/hooks.py +++ b/allianceauth/theme/hooks.py @@ -10,12 +10,12 @@ class ThemeHook: def __init__(self, name: str, description: str, - css: List[dict], - js: List[dict], - css_template: Optional[str] = None, - js_template: Optional[str] = None, - html_tags: Optional[str] = "", - header_padding: Optional[str] = "4em"): + css: list[dict], + js: list[dict], + css_template: str | None = None, + js_template: str | None = None, + html_tags: str | None = "", + header_padding: str | None = "4em"): """ :param name: Theme python name :type name: str diff --git a/allianceauth/urls.py b/allianceauth/urls.py index 71a43b26..a2505f4d 100644 --- a/allianceauth/urls.py +++ b/allianceauth/urls.py @@ -1,4 +1,5 @@ -from typing import List, Iterable, Callable +from typing import List +from collections.abc import Iterable, Callable from django.urls import include import esi.urls @@ -24,8 +25,8 @@ admin.site.site_header = NAME def urls_from_apps( - apps_hook_functions: Iterable[Callable], public_views_allowed: List[str] -) -> List[URLPattern]: + apps_hook_functions: Iterable[Callable], public_views_allowed: list[str] +) -> list[URLPattern]: """Return urls from apps and add default decorators.""" url_patterns = [] diff --git a/allianceauth/utils/counters.py b/allianceauth/utils/counters.py index 095ea13d..c84c5527 100644 --- a/allianceauth/utils/counters.py +++ b/allianceauth/utils/counters.py @@ -22,7 +22,7 @@ class ItemCounter: DEFAULT_CACHE_TIMEOUT = 24 * 3600 def __init__( - self, name: str, minimum: Optional[int] = None, redis: Optional[Redis] = None + self, name: str, minimum: int | None = None, redis: Redis | None = None ) -> None: if not name: raise ValueError("Must define a name") @@ -60,6 +60,6 @@ class ItemCounter: except ValueError: pass - def value(self) -> Optional[int]: + def value(self) -> int | None: """Return current value or None if not yet initialized.""" return cache.get(self._cache_key) From 3a5b84d1f9d70e06f437c4b922ebcb3e2052d3da Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 13:42:55 +1000 Subject: [PATCH 008/178] remove \u2060 from image name --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cc8a1370..2c0e7f76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -89,7 +89,7 @@ test-3.12-core: test-3.13-core: <<: *only-default - image: python:3.13-rc-bookworm⁠ + image: python:3.13-rc-bookworm script: - tox -e py313-core artifacts: @@ -139,7 +139,7 @@ test-3.12-all: test-3.13-all: <<: *only-default - image: python:3.13-rc-bookworm⁠ + image: python:3.13-rc-bookworm script: - tox -e py313-all artifacts: From 29ad4acff7c8d55c9aca6bba48d6955813f55b1b Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 15:08:41 +1000 Subject: [PATCH 009/178] bring docs up to python 312 across the board --- .readthedocs.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index b3b00bfd..34be14fc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,11 +7,11 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 apt_packages: - redis tools: - python: "3.11" + python: "3.12" jobs: post_system_dependencies: - redis-server --daemonize yes diff --git a/tox.ini b/tox.ini index d8f96955..d9c30ac2 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ commands = [testenv:docs] description = invoke sphinx-build to build the HTML docs -basepython = python3.11 +basepython = python3.12 install_command = pip install -e ".[docs]" -U {opts} {packages} commands = sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs} From 0839920032d0afba8d88a7a420edf12ba063c499 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 15:09:02 +1000 Subject: [PATCH 010/178] use defined pygments --- docs/features/services/mumble-docker.md | 2 +- docs/features/services/openfire-docker.md | 2 +- docs/features/services/teamspeak3-docker.md | 2 +- docs/installation-containerized/v4_docker_migration.md | 4 ++-- docs/installation/apache.md | 2 +- docs/maintenance/tuning/web.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/features/services/mumble-docker.md b/docs/features/services/mumble-docker.md index 09df8458..bd8498af 100644 --- a/docs/features/services/mumble-docker.md +++ b/docs/features/services/mumble-docker.md @@ -18,7 +18,7 @@ MUMBLE_URL = "mumble.example.com" Add the following lines to your `.env` file -```env +```bash # Mumble MUMBLE_SUPERUSER_PASSWORD = superuser_password MUMBLE_ICESECRETWRITE = icesecretwrite diff --git a/docs/features/services/openfire-docker.md b/docs/features/services/openfire-docker.md index e2947f24..2f689ea0 100644 --- a/docs/features/services/openfire-docker.md +++ b/docs/features/services/openfire-docker.md @@ -25,7 +25,7 @@ BROADCAST_SERVICE_NAME = "broadcast" Add the following lines to your `.env` file -```env +```bash # Openfire OPENFIRE_SECRET_KEY = superuser_password BROADCAST_USER_PASSWORD = icesecretwrite diff --git a/docs/features/services/teamspeak3-docker.md b/docs/features/services/teamspeak3-docker.md index 81c2f24d..e07cfb41 100644 --- a/docs/features/services/teamspeak3-docker.md +++ b/docs/features/services/teamspeak3-docker.md @@ -34,7 +34,7 @@ CELERYBEAT_SCHEDULE['run_ts3_group_update'] = { Add the following lines to your `.env` file -```env +```bash # Temspeak TEAMSPEAK3_SERVERQUERY_USER = "serverquery" TEAMSPEAK3_SERVERQUERY_PASSWORD = "" diff --git a/docs/installation-containerized/v4_docker_migration.md b/docs/installation-containerized/v4_docker_migration.md index 105981ad..68882a48 100644 --- a/docs/installation-containerized/v4_docker_migration.md +++ b/docs/installation-containerized/v4_docker_migration.md @@ -29,7 +29,7 @@ Replace your docker-compose.yml with the contents of ServerName auth.example.com diff --git a/docs/maintenance/tuning/web.md b/docs/maintenance/tuning/web.md index b4241bbf..74bee60e 100644 --- a/docs/maintenance/tuning/web.md +++ b/docs/maintenance/tuning/web.md @@ -99,7 +99,7 @@ Replace the nginx service in your docker-compose as follows Modify your nginx.conf as follows -```conf +```nginx load_module modules/ngx_http_brotli_static_module.so; load_module modules/ngx_http_brotli_filter_module.so; ... From a065f043ebe1778d217a8d1d5cb0f56b7d15c19b Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 15:09:12 +1000 Subject: [PATCH 011/178] correct index for new page name --- docs/maintenance/tuning/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maintenance/tuning/index.md b/docs/maintenance/tuning/index.md index 56cef0f7..82ed67f1 100644 --- a/docs/maintenance/tuning/index.md +++ b/docs/maintenance/tuning/index.md @@ -9,7 +9,7 @@ Tuning usually has benefits and costs and should only be performed by experience :::{toctree} :maxdepth: 1 -gunicorn +web celery redis python From 235675fa9bf140bb87c255cef302d978bfb8d601 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 15:41:37 +1000 Subject: [PATCH 012/178] django.utils.timezone.utc was removed. (shame it was handy) --- allianceauth/checks.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index 657c7313..704f6e6e 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -1,4 +1,3 @@ -from typing import List from django import db from django.core.checks import CheckMessage, Error, register, Warning from allianceauth.utils.cache import get_redis_client @@ -7,7 +6,7 @@ from packaging.version import InvalidVersion, Version as Pep440Version from celery import current_app from django.conf import settings from sqlite3.dbapi2 import sqlite_version_info - +import datetime """ A = System Packages B = Configuration @@ -41,7 +40,7 @@ def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: errors.append(Warning("Unable to confirm Redis Version")) return errors - if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc): + if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=datetime.timezone.utc): errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001")) elif redis_version.major == 7 and redis_version.minor == 0: errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002")) @@ -66,7 +65,7 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: return errors # MySQL 8 - if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc): + if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=datetime.timezone.utc): errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004")) elif mysql_version.major == 8 and mysql_version.minor == 3: errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005")) @@ -74,7 +73,7 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006")) elif mysql_version.major == 8 and mysql_version.minor == 1: errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007")) - elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc): + elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=datetime.timezone.utc): errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008")) elif mysql_version.major < 8: # This will also catch Mariadb 5.x errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A009")) @@ -94,25 +93,25 @@ def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: return errors # MariaDB 11 - if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc): + if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010")) elif mariadb_version.major == 11 and mariadb_version.minor == 2: errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018")) - if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc): + if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011")) elif mariadb_version.major == 11 and mariadb_version.minor == 1: errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019")) - if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc): + if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012")) elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013")) # MariaDB 10 - elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=timezone.utc): + elif mariadb_version.major == 10 and mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A014")) - elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc): + elif mariadb_version.major == 10 and mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A0015")) - elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=timezone.utc): + elif mariadb_version.major == 10 and mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A016")) elif mariadb_version.major == 10 and mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A017")) From 63fb449060358d744607b971cfe186114c463c0a Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 24 Aug 2024 15:42:11 +1000 Subject: [PATCH 013/178] django now doesnt like unsaved models, fair --- allianceauth/services/modules/discord/tests/test_managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/services/modules/discord/tests/test_managers.py b/allianceauth/services/modules/discord/tests/test_managers.py index 90e87532..8b65fd26 100644 --- a/allianceauth/services/modules/discord/tests/test_managers.py +++ b/allianceauth/services/modules/discord/tests/test_managers.py @@ -438,7 +438,7 @@ class TestUserHasAccount(NoSocketsTestCase): self.assertFalse(DiscordUser.objects.user_has_account(self.user)) def test_return_false_if_user_does_not_exist(self): - my_user = User(username='Dummy') + my_user = AuthUtils.create_user("test_return_false_if_user_does_not_exist") self.assertFalse(DiscordUser.objects.user_has_account(my_user)) def test_return_false_if_not_called_with_user_object(self): From 98509b0dbfd874b64f7f515d121b04fe758a5c9a Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Fri, 13 Sep 2024 10:50:15 +0000 Subject: [PATCH 014/178] Linting and Formatting changes for 5.x --- .gitignore | 1 + .pre-commit-config.yaml | 20 ++++++------ pyproject.toml | 70 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 0ede2ddf..3d0b061c 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ celerybeat-schedule #other .flake8 +.ruff_cache .pylintrc Makefile alliance_auth.sqlite3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01bb6c7d..dc329454 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,14 +4,14 @@ # pre-commit autoupdate repos: - # Code Upgrades - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.4 hooks: - - id: pyupgrade - args: [--py310-plus] + # Run the linter, and only the linter + - id: ruff + - repo: https://github.com/adamchainz/django-upgrade - rev: 1.17.0 + rev: 1.21.0 hooks: - id: django-upgrade args: [--target-version=4.2] @@ -63,7 +63,7 @@ repos: swagger\.json ) - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 2.7.3 + rev: 3.0.3 hooks: - id: editorconfig-checker exclude: | @@ -82,7 +82,7 @@ repos: - --disable=MD013 # Infrastructure - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.1.3 + rev: 2.2.3 hooks: - id: pyproject-fmt name: pyproject.toml formatter @@ -90,9 +90,9 @@ repos: args: - --indent=4 additional_dependencies: - - tox==4.15.0 # https://github.com/tox-dev/tox/releases/latest + - tox==4.18.1 # https://github.com/tox-dev/tox/releases/latest - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.19 hooks: - id: validate-pyproject name: Validate pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index b402051c..68092df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,47 @@ scripts.allianceauth = "allianceauth.bin.allianceauth:main" [tool.flit.module] name = "allianceauth" +[tool.ruff] +line-length = 119 +format.line-ending = "lf" +lint.select = [ + "B", # flake8-bugbear + "C", # pylint convention + # "D", # pydocstyle, Want to turn these on, but our docstrings are lightly used + "D300", # use triple double-quotes in docstrings PEP 257 + "DJ", # flake8-django + "DOC", # pylintdoc + "E", # pycodestyle error + "F", # pyflakes (flake8 base) + "G010", # logging-warn, Warn on using logging.warn + "I", # isort + "PGH005", # pygrep-hooks, python-check-mock-method + "RUF100", # basically yesqa + "UP", # pyupgrade, will target requires-python + "W", # pycodestyle Warning +] +lint.ignore = [ + "E501", # Line too long, WIP across repo. +] +lint.isort.combine-as-imports = true # profile=django +lint.isort.section-order = [ + "future", + "standard-library", + "third-party", + "DJANGO", + "ESI", + "first-party", + "local-folder", +] +lint.isort.sections."DJANGO" = [ + "django", + "django_redis", + "django_registration", +] +lint.isort.sections."ESI" = [ + "esi", +] + [tool.isort] profile = "django" sections = [ @@ -99,7 +140,34 @@ sections = [ known_esi = [ "esi", ] -known_django = [ +known_django = [ #Lump relatively "django", + "django_redis", + "django_registration", ] skip_gitignore = true + +[tool.flake8] +exclude = [ + ".git", + "*migrations*", + ".tox", + "dist", + "htmlcov", +] +max-line-length = 119 +select = [ + "C", # pylint convention + "E", # pycodestyle error + "F", # pyflakes (flake8 base) + "W", # pycodestyle Warning + "B", # flake8-bugbear +] +ignore = [ + "E501", # Line too long, WIP across repo. +] + +[tool.djlint] +max_attribute_length = 119 +max_line_length = 119 +max_blank_lines = 1 From ec5cf08eefbc55a6086e89f201c49b3f9891a36d Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Fri, 13 Sep 2024 23:09:12 +1000 Subject: [PATCH 015/178] update classifiers and dependencies --- pyproject.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 68092df9..18b47caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Environment :: Web Environment", "Framework :: Celery", "Framework :: Django", - "Framework :: Django :: 4.2", + "Framework :: Django :: 5.1", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Operating System :: POSIX :: Linux", @@ -39,15 +39,15 @@ dynamic = [ dependencies = [ "bcrypt", "beautifulsoup4", - "celery<6,>=5.2", + "celery<6,>=5.4", "celery-once>=3.0.1", "django<5.2,>=5.1", "django-bootstrap-form", "django-bootstrap5>=23.3", "django-celery-beat>=2.7", "django-esi>=5", - "django-redis>=5.2", - "django-registration<3.4,>=3.3", + "django-redis>=5.4", + "django-registration<4,>=3.3", "django-solo", "django-sortedm2m", "dnspython", @@ -57,17 +57,17 @@ dependencies = [ "passlib", "pydiscourse", "python-slugify>=1.2", - "redis>=4", + "redis>=5", "requests>=2.9.1", "requests-oauthlib", "semantic-version", "slixmpp", ] optional-dependencies.docs = [ - "myst-parser", - "sphinx", + "myst-parser>=4", + "sphinx>=8", "sphinx-copybutton", - "sphinx-rtd-theme<3,>=2", + "sphinx-rtd-theme<4,>=3.0.0rc1", "sphinx-tabs", "sphinxcontrib-django", ] From a99315ea55339f0b6010b5c9d8703e51278fcf29 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Fri, 13 Sep 2024 23:10:37 +1000 Subject: [PATCH 016/178] formatting storm --- .../analytics/migrations/0001_initial.py | 3 +- .../migrations/0002_add_AA_Team_Token.py | 2 +- .../migrations/0003_Generate_Identifier.py | 1 - .../0008_add_AA_GA-4_Team_Token .py | 4 +- allianceauth/analytics/models.py | 14 ++- allianceauth/analytics/tasks.py | 16 +-- allianceauth/analytics/tests/test_models.py | 9 +- allianceauth/analytics/tests/test_tasks.py | 5 +- allianceauth/analytics/tests/test_utils.py | 7 +- allianceauth/analytics/utils.py | 4 +- allianceauth/apps.py | 1 - allianceauth/authentication/admin.py | 39 ++---- allianceauth/authentication/app_settings.py | 2 +- allianceauth/authentication/apps.py | 2 +- allianceauth/authentication/auth_hooks.py | 5 +- allianceauth/authentication/backends.py | 5 +- allianceauth/authentication/checks.py | 2 +- .../authentication/core/celery_workers.py | 1 - allianceauth/authentication/decorators.py | 8 +- allianceauth/authentication/forms.py | 2 +- allianceauth/authentication/hmac_urls.py | 3 +- .../management/commands/checkmains.py | 4 +- allianceauth/authentication/managers.py | 2 +- allianceauth/authentication/middleware.py | 4 +- .../authentication/migrations/0001_initial.py | 2 +- .../migrations/0004_create_permissions.py | 1 + .../migrations/0005_delete_perms.py | 1 + .../0010_only_one_authservicesinfo.py | 1 + ...011_authservicesinfo_user_onetoonefield.py | 2 +- ...add_delete_authservicesinfo_permissions.py | 3 +- .../migrations/0013_service_modules.py | 4 +- .../migrations/0015_user_profiles.py | 3 +- .../migrations/0016_ownershiprecord.py | 2 +- allianceauth/authentication/models.py | 29 +++-- allianceauth/authentication/signals.py | 16 +-- .../task_statistics/counters.py | 2 +- .../task_statistics/event_series.py | 1 - .../authentication/task_statistics/signals.py | 6 +- .../task_statistics/tests/test_counters.py | 5 +- .../task_statistics/tests/test_helpers.py | 3 +- .../task_statistics/tests/test_signals.py | 2 +- allianceauth/authentication/tasks.py | 10 +- allianceauth/authentication/tests/__init__.py | 13 +- .../tests/core/test_celery_workers.py | 3 +- .../authentication/tests/test_admin.py | 33 +++-- .../authentication/tests/test_app_settings.py | 5 +- .../authentication/tests/test_backend.py | 8 +- .../authentication/tests/test_decorators.py | 9 +- .../authentication/tests/test_middleware.py | 16 +-- .../authentication/tests/test_models.py | 6 +- .../authentication/tests/test_signals.py | 18 +-- .../authentication/tests/test_templatetags.py | 12 +- .../authentication/tests/test_views.py | 7 +- allianceauth/authentication/views.py | 26 ++-- allianceauth/bin/allianceauth.py | 5 +- allianceauth/checks.py | 25 ++-- allianceauth/context_processors.py | 1 + allianceauth/corputils/admin.py | 2 +- allianceauth/corputils/auth_hooks.py | 5 +- allianceauth/corputils/managers.py | 7 +- .../corputils/migrations/0001_initial.py | 2 +- .../migrations/0002_migrate_permissions.py | 2 +- .../migrations/0004_member_models.py | 5 +- allianceauth/corputils/models.py | 21 ++-- allianceauth/corputils/tasks.py | 1 + allianceauth/corputils/tests.py | 16 ++- allianceauth/corputils/urls.py | 1 + allianceauth/corputils/views.py | 9 +- allianceauth/custom_css/admin.py | 7 +- allianceauth/custom_css/forms.py | 6 +- allianceauth/custom_css/models.py | 1 - .../custom_css/templatetags/custom_css.py | 4 +- allianceauth/custom_css/widgets.py | 1 - allianceauth/eveonline/admin.py | 7 +- allianceauth/eveonline/autogroups/admin.py | 8 +- allianceauth/eveonline/autogroups/apps.py | 2 +- .../autogroups/migrations/0001_initial.py | 2 +- allianceauth/eveonline/autogroups/models.py | 13 +- allianceauth/eveonline/autogroups/signals.py | 6 +- .../eveonline/autogroups/tests/__init__.py | 5 +- .../autogroups/tests/test_managers.py | 1 + .../eveonline/autogroups/tests/test_models.py | 10 +- .../autogroups/tests/test_signals.py | 8 +- allianceauth/eveonline/evelinks/dotlan.py | 10 +- .../eveonline/evelinks/eveimageserver.py | 21 +--- allianceauth/eveonline/evelinks/evewho.py | 3 +- .../eveonline/evelinks/tests/test_evelinks.py | 4 +- .../evelinks/tests/test_templatetags.py | 4 +- allianceauth/eveonline/evelinks/zkillboard.py | 5 +- allianceauth/eveonline/managers.py | 1 + .../eveonline/migrations/0001_initial.py | 2 +- .../migrations/0003_auto_20161026_0149.py | 2 +- .../eveonline/migrations/0009_on_delete.py | 2 +- allianceauth/eveonline/models.py | 22 ++-- allianceauth/eveonline/providers.py | 4 +- allianceauth/eveonline/tasks.py | 3 +- .../eveonline/templatetags/evelinks.py | 12 +- .../eveonline/tests/esi_client_stub.py | 2 +- allianceauth/eveonline/tests/test_managers.py | 6 +- allianceauth/eveonline/tests/test_models.py | 7 +- .../eveonline/tests/test_providers.py | 18 +-- allianceauth/fleetactivitytracking/admin.py | 2 +- .../fleetactivitytracking/auth_hooks.py | 6 +- .../migrations/0002_auto_20160905_2220.py | 1 + .../migrations/0003_auto_20160906_2354.py | 2 +- allianceauth/fleetactivitytracking/models.py | 2 +- allianceauth/fleetactivitytracking/urls.py | 1 + allianceauth/fleetactivitytracking/views.py | 51 ++++---- allianceauth/framework/api/evecharacter.py | 1 - allianceauth/framework/api/user.py | 1 - allianceauth/framework/tests/test_api_user.py | 4 +- allianceauth/groupmanagement/admin.py | 11 +- allianceauth/groupmanagement/auth_hooks.py | 4 +- allianceauth/groupmanagement/managers.py | 2 +- .../migrations/0001_initial.py | 2 +- .../migrations/0002_auto_20160906_2354.py | 2 +- .../migrations/0003_default_groups.py | 2 +- .../migrations/0004_authgroup.py | 6 +- .../migrations/0006_request_groups_perm.py | 7 +- .../migrations/0009_requestlog.py | 2 +- .../migrations/0011_requestlog_date.py | 1 + .../migrations/0018_reservedgroupname.py | 2 +- allianceauth/groupmanagement/models.py | 7 +- allianceauth/groupmanagement/signals.py | 3 +- .../groupmanagement/tests/__init__.py | 5 +- .../groupmanagement/tests/test_admin.py | 4 +- .../groupmanagement/tests/test_managers.py | 6 +- .../groupmanagement/tests/test_signals.py | 5 +- allianceauth/groupmanagement/urls.py | 1 + allianceauth/groupmanagement/views.py | 61 ++++------ allianceauth/hooks.py | 9 +- allianceauth/hrapplications/admin.py | 10 +- allianceauth/hrapplications/managers.py | 2 +- .../hrapplications/migrations/0001_initial.py | 2 +- .../migrations/0002_choices_for_questions.py | 2 +- .../migrations/0005_sorted_questions.py | 3 +- allianceauth/hrapplications/models.py | 16 +-- allianceauth/hrapplications/tests.py | 2 +- allianceauth/hrapplications/views.py | 40 +++--- allianceauth/menu/admin.py | 1 - allianceauth/menu/core/menu_item_hooks.py | 2 +- allianceauth/menu/hooks.py | 1 - allianceauth/menu/migrations/0001_initial.py | 2 +- allianceauth/menu/models.py | 9 +- .../menu/templatetags/menu_menu_items.py | 1 - .../templatetags/test_menu_menu_items.py | 2 +- allianceauth/notifications/admin.py | 1 + allianceauth/notifications/handlers.py | 3 +- allianceauth/notifications/managers.py | 2 +- .../notifications/migrations/0001_initial.py | 2 +- allianceauth/notifications/models.py | 4 +- .../templatetags/auth_notifications.py | 1 - .../notifications/tests/test_handlers.py | 5 +- allianceauth/notifications/tests/test_init.py | 1 + .../notifications/tests/test_managers.py | 2 +- .../notifications/tests/test_models.py | 2 +- .../notifications/tests/test_templatetags.py | 7 +- .../notifications/tests/test_views.py | 6 +- allianceauth/notifications/urls.py | 1 + allianceauth/notifications/views.py | 6 +- allianceauth/optimer/auth_hooks.py | 6 +- allianceauth/optimer/form_widgets.py | 6 +- .../optimer/migrations/0001_initial.py | 3 +- .../optimer/migrations/0004_on_delete.py | 2 +- .../0005_add_type_and_description.py | 2 +- allianceauth/optimer/models.py | 13 +- allianceauth/optimer/views.py | 14 +-- allianceauth/permissions_tool/auth_hooks.py | 6 +- allianceauth/permissions_tool/models.py | 3 + allianceauth/permissions_tool/tests.py | 2 + allianceauth/permissions_tool/urls.py | 3 +- allianceauth/permissions_tool/views.py | 7 +- allianceauth/project_template/manage.py | 6 +- .../project_template/project_name/__init__.py | 2 +- .../project_template/project_name/celery.py | 3 +- .../project_name/settings/base.py | 1 - .../project_template/project_name/urls.py | 3 +- allianceauth/services/abstract.py | 24 ++-- allianceauth/services/apps.py | 2 +- allianceauth/services/auth_hooks.py | 3 +- allianceauth/services/hooks.py | 9 +- .../services/modules/discord/admin.py | 2 +- allianceauth/services/modules/discord/api.py | 7 +- .../services/modules/discord/app_settings.py | 1 - .../services/modules/discord/auth_hooks.py | 7 +- allianceauth/services/modules/discord/core.py | 3 +- .../discord/discord_client/app_settings.py | 1 - .../modules/discord/discord_client/client.py | 15 +-- .../modules/discord/discord_client/helpers.py | 13 +- .../modules/discord/discord_client/models.py | 1 - .../discord/discord_client/tests/factories.py | 2 +- .../tests/piloting_concurrency.py | 6 +- .../tests/piloting_functionality.py | 6 +- .../discord_client/tests/test_client.py | 5 +- .../discord_client/tests/test_exceptions.py | 2 +- .../services/modules/discord/managers.py | 13 +- .../discord/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- .../discord/migrations/0003_big_overhaul.py | 2 +- .../services/modules/discord/models.py | 9 +- .../services/modules/discord/tasks.py | 15 +-- .../modules/discord/tests/test_admin.py | 10 +- .../modules/discord/tests/test_integration.py | 20 +-- .../modules/discord/tests/test_managers.py | 2 - .../modules/discord/tests/test_models.py | 2 +- .../services/modules/discord/utils.py | 3 +- .../services/modules/discord/views.py | 5 +- .../services/modules/discourse/admin.py | 2 +- .../services/modules/discourse/auth_hooks.py | 5 +- .../services/modules/discourse/manager.py | 18 +-- .../discourse/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- .../services/modules/discourse/models.py | 6 +- .../services/modules/discourse/providers.py | 4 +- .../services/modules/discourse/tasks.py | 12 +- .../services/modules/discourse/tests.py | 14 +-- .../services/modules/discourse/views.py | 24 ++-- .../services/modules/example/auth_hooks.py | 1 + allianceauth/services/modules/ips4/admin.py | 1 + .../services/modules/ips4/auth_hooks.py | 1 + allianceauth/services/modules/ips4/manager.py | 42 ++++--- .../modules/ips4/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- allianceauth/services/modules/ips4/models.py | 5 +- allianceauth/services/modules/ips4/tasks.py | 10 +- allianceauth/services/modules/ips4/tests.py | 9 +- allianceauth/services/modules/ips4/views.py | 37 +++--- allianceauth/services/modules/mumble/admin.py | 2 +- .../services/modules/mumble/auth_hooks.py | 7 +- .../0001_squashed_0011_auto_20201011_1009.py | 5 +- .../mumble/migrations/0003_mumbleuser_user.py | 2 +- .../migrations/0004_auto_20161214_1024.py | 2 +- .../migrations/0006_service_permissions.py | 10 +- .../mumble/migrations/0007_not_null_user.py | 2 +- .../0008_mumbleuser_display_name.py | 1 + .../0009_set_mumble_dissplay_names.py | 5 +- .../services/modules/mumble/models.py | 20 ++- allianceauth/services/modules/mumble/tasks.py | 25 ++-- allianceauth/services/modules/mumble/tests.py | 13 +- allianceauth/services/modules/mumble/views.py | 8 +- .../services/modules/openfire/admin.py | 2 +- .../services/modules/openfire/auth_hooks.py | 3 +- .../services/modules/openfire/manager.py | 36 +++--- .../openfire/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- .../services/modules/openfire/models.py | 5 +- .../services/modules/openfire/tasks.py | 17 +-- .../services/modules/openfire/tests.py | 12 +- .../services/modules/openfire/views.py | 42 +++---- allianceauth/services/modules/phpbb3/admin.py | 3 +- .../services/modules/phpbb3/auth_hooks.py | 3 +- .../services/modules/phpbb3/manager.py | 114 +++++++++--------- .../modules/phpbb3/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- allianceauth/services/modules/phpbb3/tasks.py | 15 ++- allianceauth/services/modules/phpbb3/tests.py | 15 ++- allianceauth/services/modules/phpbb3/views.py | 34 +++--- allianceauth/services/modules/smf/admin.py | 2 +- .../services/modules/smf/auth_hooks.py | 3 +- allianceauth/services/modules/smf/manager.py | 45 ++++--- .../modules/smf/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- .../0003_set_smf_displayed_names.py | 4 +- allianceauth/services/modules/smf/models.py | 6 +- allianceauth/services/modules/smf/tasks.py | 17 +-- allianceauth/services/modules/smf/tests.py | 15 ++- allianceauth/services/modules/smf/views.py | 26 ++-- .../services/modules/teamspeak3/admin.py | 8 +- .../services/modules/teamspeak3/apps.py | 2 +- .../services/modules/teamspeak3/auth_hooks.py | 5 +- .../services/modules/teamspeak3/manager.py | 57 ++++----- .../teamspeak3/migrations/0001_initial.py | 2 +- .../migrations/0003_teamspeak3user.py | 2 +- .../migrations/0004_service_permissions.py | 10 +- .../teamspeak3/migrations/0005_stategroup.py | 2 +- .../services/modules/teamspeak3/models.py | 11 +- .../services/modules/teamspeak3/signals.py | 12 +- .../services/modules/teamspeak3/tasks.py | 13 +- .../services/modules/teamspeak3/tests.py | 39 +++--- .../services/modules/teamspeak3/util/ts3.py | 12 +- .../services/modules/teamspeak3/views.py | 38 +++--- .../services/modules/xenforo/admin.py | 2 +- .../services/modules/xenforo/auth_hooks.py | 1 + .../services/modules/xenforo/manager.py | 6 +- .../xenforo/migrations/0001_initial.py | 2 +- .../migrations/0002_service_permissions.py | 10 +- .../services/modules/xenforo/models.py | 5 +- .../services/modules/xenforo/tasks.py | 1 + .../services/modules/xenforo/tests.py | 13 +- .../services/modules/xenforo/views.py | 30 ++--- allianceauth/services/signals.py | 25 ++-- allianceauth/services/tasks.py | 9 +- allianceauth/services/tests/test_hooks.py | 2 +- allianceauth/services/tests/test_models.py | 1 + .../services/tests/test_nameformatter.py | 6 +- allianceauth/services/tests/test_signals.py | 3 +- allianceauth/services/tests/test_tasks.py | 6 +- allianceauth/services/urls.py | 4 +- allianceauth/services/views.py | 12 +- allianceauth/srp/admin.py | 3 +- allianceauth/srp/managers.py | 2 +- allianceauth/srp/migrations/0001_initial.py | 2 +- allianceauth/srp/migrations/0004_on_delete.py | 2 +- allianceauth/srp/models.py | 10 +- allianceauth/srp/providers.py | 1 + allianceauth/srp/tests/test_managers.py | 6 +- allianceauth/srp/views.py | 63 +++++----- allianceauth/templatetags/admin_status.py | 8 +- allianceauth/tests/auth_utils.py | 24 ++-- allianceauth/tests/test_auth_utils.py | 9 +- allianceauth/tests/test_urls.py | 3 +- allianceauth/theme/bootstrap/auth_hooks.py | 1 - allianceauth/theme/templatetags/theme_tags.py | 2 +- .../navhelper/templatetags/navactive.py | 5 +- allianceauth/thirdparty/navhelper/tests.py | 2 +- allianceauth/timerboard/auth_hooks.py | 4 +- allianceauth/timerboard/form.py | 2 +- .../timerboard/migrations/0001_initial.py | 2 +- .../timerboard/migrations/0003_on_delete.py | 2 +- allianceauth/timerboard/models.py | 9 +- allianceauth/timerboard/tests.py | 23 ++-- allianceauth/timerboard/views.py | 6 +- allianceauth/urls.py | 12 +- allianceauth/utils/counters.py | 1 - allianceauth/utils/testing.py | 1 + allianceauth/utils/tests/test_testing.py | 1 + docker/conf/celery.py | 3 +- docker/conf/urls.py | 3 +- docs/conf.py | 4 +- manage.py | 8 +- pyproject.toml | 6 + runtests.py | 12 +- tests/celery.py | 2 +- tests/settings_all.py | 4 +- tests/settings_core.py | 2 +- tests/urls.py | 4 +- 336 files changed, 1347 insertions(+), 1370 deletions(-) diff --git a/allianceauth/analytics/migrations/0001_initial.py b/allianceauth/analytics/migrations/0001_initial.py index c5a1f33a..e6385469 100644 --- a/allianceauth/analytics/migrations/0001_initial.py +++ b/allianceauth/analytics/migrations/0001_initial.py @@ -1,8 +1,9 @@ # Generated by Django 3.1.4 on 2020-12-30 13:11 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/allianceauth/analytics/migrations/0002_add_AA_Team_Token.py b/allianceauth/analytics/migrations/0002_add_AA_Team_Token.py index 13071b8a..4acb725f 100644 --- a/allianceauth/analytics/migrations/0002_add_AA_Team_Token.py +++ b/allianceauth/analytics/migrations/0002_add_AA_Team_Token.py @@ -21,7 +21,7 @@ def remove_aa_team_token(apps, schema_editor): # Have to define some code to remove this identifier # In case of migration rollback? Tokens = apps.get_model('analytics', 'AnalyticsTokens') - token = Tokens.objects.filter(token="UA-186249766-2").delete() + Tokens.objects.filter(token="UA-186249766-2").delete() class Migration(migrations.Migration): diff --git a/allianceauth/analytics/migrations/0003_Generate_Identifier.py b/allianceauth/analytics/migrations/0003_Generate_Identifier.py index 5c4daba8..08df9ca8 100644 --- a/allianceauth/analytics/migrations/0003_Generate_Identifier.py +++ b/allianceauth/analytics/migrations/0003_Generate_Identifier.py @@ -1,6 +1,5 @@ # Generated by Django 3.1.4 on 2020-12-30 08:53 -from uuid import uuid4 from django.db import migrations diff --git a/allianceauth/analytics/migrations/0008_add_AA_GA-4_Team_Token .py b/allianceauth/analytics/migrations/0008_add_AA_GA-4_Team_Token .py index efc816ac..db7e8afd 100644 --- a/allianceauth/analytics/migrations/0008_add_AA_GA-4_Team_Token .py +++ b/allianceauth/analytics/migrations/0008_add_AA_GA-4_Team_Token .py @@ -1,7 +1,7 @@ # Generated by Django 3.1.4 on 2020-12-30 08:53 -from django.db import migrations from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations def add_aa_team_token(apps, schema_editor): @@ -51,7 +51,7 @@ def remove_aa_team_token(apps, schema_editor): # Have to define some code to remove this identifier # In case of migration rollback? Tokens = apps.get_model('analytics', 'AnalyticsTokens') - token = Tokens.objects.filter(token="G-6LYSMYK8DE").delete() + Tokens.objects.filter(token="G-6LYSMYK8DE").delete() class Migration(migrations.Migration): diff --git a/allianceauth/analytics/models.py b/allianceauth/analytics/models.py index ddee495a..72d11abe 100644 --- a/allianceauth/analytics/models.py +++ b/allianceauth/analytics/models.py @@ -1,9 +1,9 @@ -from django.db import models -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ - from uuid import uuid4 +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + class AnalyticsIdentifier(models.Model): @@ -11,6 +11,9 @@ class AnalyticsIdentifier(models.Model): default=uuid4, editable=False) + def __str__(self) -> str: + return f"{self.identifier}" + def save(self, *args, **kwargs): if not self.pk and AnalyticsIdentifier.objects.exists(): # Force a single object @@ -31,3 +34,6 @@ class AnalyticsTokens(models.Model): token = models.CharField(max_length=254, blank=False) secret = models.CharField(max_length=254, blank=True) send_stats = models.BooleanField(default=False) + + def __str__(self) -> str: + return self.name diff --git a/allianceauth/analytics/tasks.py b/allianceauth/analytics/tasks.py index cc9ef160..00ed8202 100644 --- a/allianceauth/analytics/tasks.py +++ b/allianceauth/analytics/tasks.py @@ -1,16 +1,16 @@ -import requests import logging -from django.conf import settings -from django.apps import apps + +import requests from celery import shared_task -from .models import AnalyticsTokens, AnalyticsIdentifier -from .utils import ( - install_stat_addons, - install_stat_tokens, - install_stat_users) + +from django.apps import apps +from django.conf import settings from allianceauth import __version__ +from .models import AnalyticsIdentifier, AnalyticsTokens +from .utils import install_stat_addons, install_stat_tokens, install_stat_users + logger = logging.getLogger(__name__) BASE_URL = "https://www.google-analytics.com" diff --git a/allianceauth/analytics/tests/test_models.py b/allianceauth/analytics/tests/test_models.py index 452c4eaa..b430f03d 100644 --- a/allianceauth/analytics/tests/test_models.py +++ b/allianceauth/analytics/tests/test_models.py @@ -1,10 +1,9 @@ -from allianceauth.analytics.models import AnalyticsIdentifier -from django.core.exceptions import ValidationError - -from django.test.testcases import TestCase - from uuid import UUID, uuid4 +from django.core.exceptions import ValidationError +from django.test.testcases import TestCase + +from allianceauth.analytics.models import AnalyticsIdentifier # Identifiers uuid_1 = "ab33e241fbf042b6aa77c7655a768af7" diff --git a/allianceauth/analytics/tests/test_tasks.py b/allianceauth/analytics/tests/test_tasks.py index 0a542e23..08a5676a 100644 --- a/allianceauth/analytics/tests/test_tasks.py +++ b/allianceauth/analytics/tests/test_tasks.py @@ -2,12 +2,9 @@ import requests_mock from django.test.utils import override_settings -from allianceauth.analytics.tasks import ( - analytics_event, - send_ga_tracking_celery_event) +from allianceauth.analytics.tasks import analytics_event, send_ga_tracking_celery_event from allianceauth.utils.testing import NoSocketsTestCase - GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/mp/collect' diff --git a/allianceauth/analytics/tests/test_utils.py b/allianceauth/analytics/tests/test_utils.py index b9a22329..838513d8 100644 --- a/allianceauth/analytics/tests/test_utils.py +++ b/allianceauth/analytics/tests/test_utils.py @@ -1,10 +1,9 @@ from django.apps import apps -from allianceauth.authentication.models import User -from esi.models import Token -from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons - from django.test.testcases import TestCase +from allianceauth.analytics.utils import install_stat_addons, install_stat_users +from allianceauth.authentication.models import User + def create_testdata(): User.objects.all().delete() diff --git a/allianceauth/analytics/utils.py b/allianceauth/analytics/utils.py index e8c57927..d09cc65c 100644 --- a/allianceauth/analytics/utils.py +++ b/allianceauth/analytics/utils.py @@ -1,7 +1,9 @@ from django.apps import apps -from allianceauth.authentication.models import User + from esi.models import Token +from allianceauth.authentication.models import User + def install_stat_users() -> int: """Count and Return the number of User accounts diff --git a/allianceauth/apps.py b/allianceauth/apps.py index 098f50ba..1191fed9 100644 --- a/allianceauth/apps.py +++ b/allianceauth/apps.py @@ -1,5 +1,4 @@ from django.apps import AppConfig -from django.core.checks import Warning, Error, register class AllianceAuthConfig(AppConfig): diff --git a/allianceauth/authentication/admin.py b/allianceauth/authentication/admin.py index 6da117e2..9729e200 100644 --- a/allianceauth/authentication/admin.py +++ b/allianceauth/authentication/admin.py @@ -1,43 +1,21 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission as BasePermission -from django.contrib.auth.models import User as BaseUser +from django.contrib.auth.models import Group, Permission as BasePermission, User as BaseUser from django.db.models import Count, Q from django.db.models.functions import Lower -from django.db.models.signals import ( - m2m_changed, - post_delete, - post_save, - pre_delete, - pre_save -) +from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save from django.dispatch import receiver from django.urls import reverse from django.utils.html import format_html from django.utils.text import slugify -from allianceauth.authentication.models import ( - CharacterOwnership, - OwnershipRecord, - State, - UserProfile, - get_guest_state -) -from allianceauth.eveonline.models import ( - EveAllianceInfo, - EveCharacter, - EveCorporationInfo, - EveFactionInfo -) +from allianceauth.authentication.models import CharacterOwnership, OwnershipRecord, State, UserProfile, get_guest_state +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo from allianceauth.eveonline.tasks import update_character from allianceauth.hooks import get_hooks from allianceauth.services.hooks import ServicesHook -from .app_settings import ( - AUTHENTICATION_ADMIN_USERS_MAX_CHARS, - AUTHENTICATION_ADMIN_USERS_MAX_GROUPS -) +from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_CHARS, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS from .forms import UserChangeForm, UserProfileForm @@ -132,10 +110,7 @@ def user_username(obj): To be used for all user based admin lists """ link = reverse( - 'admin:{}_{}_change'.format( - obj._meta.app_label, - type(obj).__name__.lower() - ), + f'admin:{obj._meta.app_label}_{type(obj).__name__.lower()}_change', args=(obj.pk,) ) user_obj = obj.user if hasattr(obj, 'user') else obj @@ -548,7 +523,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): if obj and obj.pk: return 'owner_hash', 'character' - return tuple() + return () @admin.register(OwnershipRecord) diff --git a/allianceauth/authentication/app_settings.py b/allianceauth/authentication/app_settings.py index 449bef08..e06d1eb8 100644 --- a/allianceauth/authentication/app_settings.py +++ b/allianceauth/authentication/app_settings.py @@ -25,7 +25,7 @@ def _clean_setting( if not required_type: required_type = type(default_value) - if min_value is None and required_type == int: + if min_value is None and required_type is int: min_value = 0 if (hasattr(settings, name) diff --git a/allianceauth/authentication/apps.py b/allianceauth/authentication/apps.py index 30e5a4f2..db52502c 100644 --- a/allianceauth/authentication/apps.py +++ b/allianceauth/authentication/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.core.checks import register, Tags +from django.core.checks import Tags, register class AuthenticationConfig(AppConfig): diff --git a/allianceauth/authentication/auth_hooks.py b/allianceauth/authentication/auth_hooks.py index b29a3300..34d62fc3 100644 --- a/allianceauth/authentication/auth_hooks.py +++ b/allianceauth/authentication/auth_hooks.py @@ -1,6 +1,7 @@ -from allianceauth.hooks import DashboardItemHook from allianceauth import hooks -from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin +from allianceauth.hooks import DashboardItemHook + +from .views import dashboard_admin, dashboard_characters, dashboard_esi_check, dashboard_groups class UserCharactersHook(DashboardItemHook): diff --git a/allianceauth/authentication/backends.py b/allianceauth/authentication/backends.py index 260dd293..7c3b00a3 100644 --- a/allianceauth/authentication/backends.py +++ b/allianceauth/authentication/backends.py @@ -1,10 +1,9 @@ import logging from django.contrib.auth.backends import ModelBackend -from django.contrib.auth.models import User, Permission - -from .models import UserProfile, CharacterOwnership, OwnershipRecord +from django.contrib.auth.models import Permission, User +from .models import CharacterOwnership, OwnershipRecord, UserProfile logger = logging.getLogger(__name__) diff --git a/allianceauth/authentication/checks.py b/allianceauth/authentication/checks.py index 1a687b62..88ea1219 100644 --- a/allianceauth/authentication/checks.py +++ b/allianceauth/authentication/checks.py @@ -1,5 +1,5 @@ -from django.core.checks import Error from django.conf import settings +from django.core.checks import Error def check_login_scopes_setting(*args, **kwargs): diff --git a/allianceauth/authentication/core/celery_workers.py b/allianceauth/authentication/core/celery_workers.py index ceb1f40d..c1e91bfe 100644 --- a/allianceauth/authentication/core/celery_workers.py +++ b/allianceauth/authentication/core/celery_workers.py @@ -2,7 +2,6 @@ import itertools import logging -from typing import Optional from amqp.exceptions import ChannelError from celery import current_app diff --git a/allianceauth/authentication/decorators.py b/allianceauth/authentication/decorators.py index 81403615..e54f34b0 100644 --- a/allianceauth/authentication/decorators.py +++ b/allianceauth/authentication/decorators.py @@ -1,15 +1,11 @@ -from django.urls import include -from django.contrib.auth.decorators import user_passes_test -from django.core.exceptions import PermissionDenied -from functools import wraps -from typing import Optional from collections.abc import Callable, Iterable +from functools import wraps -from django.urls import include from django.contrib import messages from django.contrib.auth.decorators import login_required, user_passes_test from django.core.exceptions import PermissionDenied from django.shortcuts import redirect +from django.urls import include from django.utils.translation import gettext_lazy as _ diff --git a/allianceauth/authentication/forms.py b/allianceauth/authentication/forms.py index fabff9d9..a4959abd 100644 --- a/allianceauth/authentication/forms.py +++ b/allianceauth/authentication/forms.py @@ -60,7 +60,7 @@ class UserChangeForm(BaseUserChangeForm): { "groups": _( "You are not allowed to add or remove these " - "restricted groups: %s" % restricted_names + "restricted groups: {}".format(restricted_names) ) } ) diff --git a/allianceauth/authentication/hmac_urls.py b/allianceauth/authentication/hmac_urls.py index 61bcd99b..9538582a 100644 --- a/allianceauth/authentication/hmac_urls.py +++ b/allianceauth/authentication/hmac_urls.py @@ -1,5 +1,6 @@ +from django.urls import include, path, re_path + from allianceauth.authentication import views -from django.urls import include, re_path, path urlpatterns = [ path('activate/complete/', views.activation_complete, name='registration_activation_complete'), diff --git a/allianceauth/authentication/management/commands/checkmains.py b/allianceauth/authentication/management/commands/checkmains.py index 5605687b..fad33078 100644 --- a/allianceauth/authentication/management/commands/checkmains.py +++ b/allianceauth/authentication/management/commands/checkmains.py @@ -1,4 +1,5 @@ from django.core.management.base import BaseCommand + from allianceauth.authentication.models import UserProfile @@ -11,8 +12,7 @@ class Command(BaseCommand): if profiles.exists(): for profile in profiles: self.stdout.write(self.style.ERROR( - '{} does not have an ownership. Resetting user {} main character.'.format(profile.main_character, - profile.user))) + f'{profile.main_character} does not have an ownership. Resetting user {profile.user} main character.')) profile.main_character = None profile.save() self.stdout.write(self.style.WARNING(f'Reset {profiles.count()} main characters.')) diff --git a/allianceauth/authentication/managers.py b/allianceauth/authentication/managers.py index 5959db96..3e16b3c2 100644 --- a/allianceauth/authentication/managers.py +++ b/allianceauth/authentication/managers.py @@ -1,7 +1,7 @@ import logging from django.db import transaction -from django.db.models import Manager, QuerySet, Q +from django.db.models import Manager, Q, QuerySet from allianceauth.eveonline.models import EveCharacter diff --git a/allianceauth/authentication/middleware.py b/allianceauth/authentication/middleware.py index ca28d92f..15f0b431 100644 --- a/allianceauth/authentication/middleware.py +++ b/allianceauth/authentication/middleware.py @@ -1,8 +1,8 @@ +import logging + from django.conf import settings from django.utils.deprecation import MiddlewareMixin -import logging - logger = logging.getLogger(__name__) diff --git a/allianceauth/authentication/migrations/0001_initial.py b/allianceauth/authentication/migrations/0001_initial.py index a7d3ff64..1c3a8842 100644 --- a/allianceauth/authentication/migrations/0001_initial.py +++ b/allianceauth/authentication/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:38 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/authentication/migrations/0004_create_permissions.py b/allianceauth/authentication/migrations/0004_create_permissions.py index 1f938d21..7b2042f9 100644 --- a/allianceauth/authentication/migrations/0004_create_permissions.py +++ b/allianceauth/authentication/migrations/0004_create_permissions.py @@ -2,6 +2,7 @@ from django.db import migrations + def create_permissions(apps, schema_editor): User = apps.get_model('auth', 'User') ContentType = apps.get_model('contenttypes', 'ContentType') diff --git a/allianceauth/authentication/migrations/0005_delete_perms.py b/allianceauth/authentication/migrations/0005_delete_perms.py index e870b8cd..d61a27af 100644 --- a/allianceauth/authentication/migrations/0005_delete_perms.py +++ b/allianceauth/authentication/migrations/0005_delete_perms.py @@ -2,6 +2,7 @@ from django.db import migrations + def delete_permissions(apps, schema_editor): User = apps.get_model('auth', 'User') ContentType = apps.get_model('contenttypes', 'ContentType') diff --git a/allianceauth/authentication/migrations/0010_only_one_authservicesinfo.py b/allianceauth/authentication/migrations/0010_only_one_authservicesinfo.py index a88bb301..86d6175c 100644 --- a/allianceauth/authentication/migrations/0010_only_one_authservicesinfo.py +++ b/allianceauth/authentication/migrations/0010_only_one_authservicesinfo.py @@ -2,6 +2,7 @@ from django.db import migrations + def count_completed_fields(model): return len([True for key, value in model.__dict__.items() if bool(value)]) diff --git a/allianceauth/authentication/migrations/0011_authservicesinfo_user_onetoonefield.py b/allianceauth/authentication/migrations/0011_authservicesinfo_user_onetoonefield.py index d12f2875..e1c28b19 100644 --- a/allianceauth/authentication/migrations/0011_authservicesinfo_user_onetoonefield.py +++ b/allianceauth/authentication/migrations/0011_authservicesinfo_user_onetoonefield.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2017-01-07 07:11 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/authentication/migrations/0012_remove_add_delete_authservicesinfo_permissions.py b/allianceauth/authentication/migrations/0012_remove_add_delete_authservicesinfo_permissions.py index 60fcc189..7949ab59 100644 --- a/allianceauth/authentication/migrations/0012_remove_add_delete_authservicesinfo_permissions.py +++ b/allianceauth/authentication/migrations/0012_remove_add_delete_authservicesinfo_permissions.py @@ -1,6 +1,7 @@ # Generated by Django 1.10.5 on 2017-01-12 00:59 -from django.db import migrations, models +from django.db import migrations + def remove_permissions(apps, schema_editor): ContentType = apps.get_model('contenttypes', 'ContentType') diff --git a/allianceauth/authentication/migrations/0013_service_modules.py b/allianceauth/authentication/migrations/0013_service_modules.py index f046905c..871e070b 100644 --- a/allianceauth/authentication/migrations/0013_service_modules.py +++ b/allianceauth/authentication/migrations/0013_service_modules.py @@ -1,9 +1,9 @@ # Generated by Django 1.10.2 on 2016-12-11 23:14 -from django.db import migrations - import logging +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/authentication/migrations/0015_user_profiles.py b/allianceauth/authentication/migrations/0015_user_profiles.py index 10ba691c..48ecdefb 100644 --- a/allianceauth/authentication/migrations/0015_user_profiles.py +++ b/allianceauth/authentication/migrations/0015_user_profiles.py @@ -1,11 +1,12 @@ # Generated by Django 1.10.5 on 2017-03-22 23:09 -import allianceauth.authentication.models import django.db.models.deletion from django.conf import settings from django.contrib.auth.hashers import make_password from django.db import migrations, models +import allianceauth.authentication.models + def create_guest_state(apps, schema_editor): State = apps.get_model('authentication', 'State') diff --git a/allianceauth/authentication/migrations/0016_ownershiprecord.py b/allianceauth/authentication/migrations/0016_ownershiprecord.py index b2df6d8d..91887454 100644 --- a/allianceauth/authentication/migrations/0016_ownershiprecord.py +++ b/allianceauth/authentication/migrations/0016_ownershiprecord.py @@ -1,8 +1,8 @@ # Generated by Django 2.0.4 on 2018-04-14 18:28 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion def create_initial_records(apps, schema_editor): diff --git a/allianceauth/authentication/models.py b/allianceauth/authentication/models.py index 714f1934..dd22883d 100644 --- a/allianceauth/authentication/models.py +++ b/allianceauth/authentication/models.py @@ -1,11 +1,11 @@ import logging -from django.contrib.auth.models import User, Permission +from django.contrib.auth.models import Permission, User from django.db import models, transaction from django.utils.translation import gettext_lazy as _ -from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo + +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo from allianceauth.notifications import notify -from django.conf import settings from .managers import CharacterOwnershipManager, StateManager @@ -60,8 +60,7 @@ def get_guest_state_pk(): class UserProfile(models.Model): - class Meta: - default_permissions = ('change',) + class Language(models.TextChoices): """ @@ -108,10 +107,15 @@ class UserProfile(models.Model): _("Theme"), max_length=200, blank=True, - null=True, help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps" ) + class Meta: + default_permissions = ('change',) + + def __str__(self) -> str: + return str(self.user) + def assign_state(self, state=None, commit=True): if not state: state = State.objects.get_for_user(self.user) @@ -122,7 +126,7 @@ class UserProfile(models.Model): self.save(update_fields=['state']) notify( self.user, - _('State changed to: %s' % state), + _('State changed to: {}'.format(state)), _('Your user\'s state is now: %(state)s') % ({'state': state}), 'info' @@ -137,19 +141,18 @@ class UserProfile(models.Model): sender=self.__class__, user=self.user, state=self.state ) - def __str__(self): - return str(self.user) + class CharacterOwnership(models.Model): - class Meta: - default_permissions = ('change', 'delete') - ordering = ['user', 'character__character_name'] + character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership') owner_hash = models.CharField(max_length=28, unique=True) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships') objects = CharacterOwnershipManager() - + class Meta: + default_permissions = ('change', 'delete') + ordering = ['user', 'character__character_name'] def __str__(self): return f"{self.user}: {self.character}" diff --git a/allianceauth/authentication/signals.py b/allianceauth/authentication/signals.py index dfb9ba1b..10c278a0 100644 --- a/allianceauth/authentication/signals.py +++ b/allianceauth/authentication/signals.py @@ -1,19 +1,16 @@ import logging -from .models import ( - CharacterOwnership, - UserProfile, - get_guest_state, - State, - OwnershipRecord) from django.contrib.auth.models import User from django.db.models import Q -from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed -from django.dispatch import receiver, Signal +from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save +from django.dispatch import Signal, receiver + from esi.models import Token from allianceauth.eveonline.models import EveCharacter +from .models import CharacterOwnership, OwnershipRecord, State, UserProfile, get_guest_state + logger = logging.getLogger(__name__) state_changed = Signal() @@ -108,8 +105,7 @@ def record_character_ownership(sender, instance, created, *args, **kwargs): def validate_main_character(sender, instance, *args, **kwargs): try: if instance.user.profile.main_character == instance.character: - logger.info("Ownership of a main character {} has been revoked. Resetting {} main character.".format( - instance.character, instance.user)) + logger.info(f"Ownership of a main character {instance.character} has been revoked. Resetting {instance.user} main character.") # clear main character as user no longer owns them instance.user.profile.main_character = None instance.user.profile.save() diff --git a/allianceauth/authentication/task_statistics/counters.py b/allianceauth/authentication/task_statistics/counters.py index 6d1f6175..3d146343 100644 --- a/allianceauth/authentication/task_statistics/counters.py +++ b/allianceauth/authentication/task_statistics/counters.py @@ -1,7 +1,7 @@ """Counters for Task Statistics.""" import datetime as dt -from typing import NamedTuple, Optional +from typing import NamedTuple from .event_series import EventSeries diff --git a/allianceauth/authentication/task_statistics/event_series.py b/allianceauth/authentication/task_statistics/event_series.py index 3eb96abe..b602ef04 100644 --- a/allianceauth/authentication/task_statistics/event_series.py +++ b/allianceauth/authentication/task_statistics/event_series.py @@ -2,7 +2,6 @@ import datetime as dt import logging -from typing import List, Optional from pytz import utc from redis import Redis diff --git a/allianceauth/authentication/task_statistics/signals.py b/allianceauth/authentication/task_statistics/signals.py index e9d7babc..5f12bfac 100644 --- a/allianceauth/authentication/task_statistics/signals.py +++ b/allianceauth/authentication/task_statistics/signals.py @@ -1,7 +1,11 @@ """Signals for Task Statistics.""" from celery.signals import ( - task_failure, task_internal_error, task_retry, task_success, worker_ready, + task_failure, + task_internal_error, + task_retry, + task_success, + worker_ready, ) from django.conf import settings diff --git a/allianceauth/authentication/task_statistics/tests/test_counters.py b/allianceauth/authentication/task_statistics/tests/test_counters.py index 2d2555aa..96494b4c 100644 --- a/allianceauth/authentication/task_statistics/tests/test_counters.py +++ b/allianceauth/authentication/task_statistics/tests/test_counters.py @@ -4,7 +4,10 @@ from django.test import TestCase from django.utils.timezone import now from allianceauth.authentication.task_statistics.counters import ( - dashboard_results, failed_tasks, retried_tasks, succeeded_tasks, + dashboard_results, + failed_tasks, + retried_tasks, + succeeded_tasks, ) diff --git a/allianceauth/authentication/task_statistics/tests/test_helpers.py b/allianceauth/authentication/task_statistics/tests/test_helpers.py index 51dae201..45edffd7 100644 --- a/allianceauth/authentication/task_statistics/tests/test_helpers.py +++ b/allianceauth/authentication/task_statistics/tests/test_helpers.py @@ -4,7 +4,8 @@ from unittest.mock import patch from redis import RedisError from allianceauth.authentication.task_statistics.helpers import ( - _RedisStub, get_redis_client_or_stub, + _RedisStub, + get_redis_client_or_stub, ) MODULE_PATH = "allianceauth.authentication.task_statistics.helpers" diff --git a/allianceauth/authentication/task_statistics/tests/test_signals.py b/allianceauth/authentication/task_statistics/tests/test_signals.py index 1e1634bd..d04f11e2 100644 --- a/allianceauth/authentication/task_statistics/tests/test_signals.py +++ b/allianceauth/authentication/task_statistics/tests/test_signals.py @@ -10,8 +10,8 @@ from allianceauth.authentication.task_statistics.counters import ( succeeded_tasks, ) from allianceauth.authentication.task_statistics.signals import ( - reset_counters, is_enabled, + reset_counters, ) from allianceauth.eveonline.tasks import update_character diff --git a/allianceauth/authentication/tasks.py b/allianceauth/authentication/tasks.py index eaf3c59a..ff5f7e63 100644 --- a/allianceauth/authentication/tasks.py +++ b/allianceauth/authentication/tasks.py @@ -1,9 +1,10 @@ import logging -from esi.errors import TokenExpiredError, TokenInvalidError, IncompleteResponseError -from esi.models import Token from celery import shared_task +from esi.errors import IncompleteResponseError, TokenExpiredError, TokenInvalidError +from esi.models import Token + from allianceauth.authentication.models import CharacterOwnership logger = logging.getLogger(__name__) @@ -22,8 +23,7 @@ def check_character_ownership(owner_hash): continue except (KeyError, IncompleteResponseError): # We can't validate the hash hasn't changed but also can't assume it has. Abort for now. - logger.warning("Failed to validate owner hash of {} due to problems contacting SSO servers.".format( - tokens[0].character_name)) + logger.warning(f"Failed to validate owner hash of {tokens[0].character_name} due to problems contacting SSO servers.") break if not t.character_owner_hash == old_hash: @@ -33,7 +33,7 @@ def check_character_ownership(owner_hash): break if not Token.objects.filter(character_owner_hash=owner_hash).exists(): - logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash) + logger.info(f'No tokens found with owner hash {owner_hash}. Revoking ownership.') CharacterOwnership.objects.filter(owner_hash=owner_hash).delete() diff --git a/allianceauth/authentication/tests/__init__.py b/allianceauth/authentication/tests/__init__.py index 13410df4..55fd9fe9 100644 --- a/allianceauth/authentication/tests/__init__.py +++ b/allianceauth/authentication/tests/__init__.py @@ -1,12 +1,7 @@ -from django.db.models.signals import ( - m2m_changed, - post_save, - pre_delete, - pre_save -) -from django.urls import reverse from unittest import mock +from django.urls import reverse + MODULE_PATH = 'allianceauth.authentication' @@ -17,9 +12,7 @@ def patch(target, *args, **kwargs): def get_admin_change_view_url(obj: object) -> str: """returns URL to admin change view for given object""" return reverse( - 'admin:{}_{}_change'.format( - obj._meta.app_label, type(obj).__name__.lower() - ), + f'admin:{obj._meta.app_label}_{type(obj).__name__.lower()}_change', args=(obj.pk,) ) diff --git a/allianceauth/authentication/tests/core/test_celery_workers.py b/allianceauth/authentication/tests/core/test_celery_workers.py index 95bec08d..5551c318 100644 --- a/allianceauth/authentication/tests/core/test_celery_workers.py +++ b/allianceauth/authentication/tests/core/test_celery_workers.py @@ -5,7 +5,8 @@ from amqp.exceptions import ChannelError from django.test import TestCase from allianceauth.authentication.core.celery_workers import ( - active_tasks_count, queued_tasks_count, + active_tasks_count, + queued_tasks_count, ) MODULE_PATH = "allianceauth.authentication.core.celery_workers" diff --git a/allianceauth/authentication/tests/test_admin.py b/allianceauth/authentication/tests/test_admin.py index 0f43a14c..26d389b7 100644 --- a/allianceauth/authentication/tests/test_admin.py +++ b/allianceauth/authentication/tests/test_admin.py @@ -1,42 +1,37 @@ -from bs4 import BeautifulSoup +from unittest.mock import MagicMock, patch from urllib.parse import quote -from unittest.mock import patch, MagicMock +from bs4 import BeautifulSoup from django_webtest import WebTest from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import Group -from django.test import TestCase, RequestFactory, Client +from django.test import Client, RequestFactory, TestCase -from allianceauth.authentication.models import ( - CharacterOwnership, State, OwnershipRecord -) -from allianceauth.eveonline.models import ( - EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo -) +from allianceauth.authentication.models import CharacterOwnership, OwnershipRecord, State +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo from allianceauth.services.hooks import ServicesHook from allianceauth.tests.auth_utils import AuthUtils from ..admin import ( BaseUserAdmin, CharacterOwnershipAdmin, - StateAdmin, - MainCorporationsFilter, MainAllianceFilter, + MainCorporationsFilter, MainFactionFilter, OwnershipRecordAdmin, + StateAdmin, User, UserAdmin, + make_service_hooks_sync_nickname_action, + make_service_hooks_update_groups_action, + update_main_character_model, user_main_organization, user_profile_pic, user_username, - update_main_character_model, - make_service_hooks_update_groups_action, - make_service_hooks_sync_nickname_action ) from . import get_admin_change_view_url, get_admin_search_url - MODULE_PATH = 'allianceauth.authentication.admin' @@ -327,15 +322,15 @@ class TestUserAdmin(TestCaseWithTestData): def test_user_username_u1(self): expected = ( - '' - 'Bruce_Wayne
Bruce Wayne'.format(self.user_1.pk) + f'' + 'Bruce_Wayne
Bruce Wayne' ) self.assertEqual(user_username(self.user_1), expected) def test_user_username_u3(self): expected = ( - '' - 'Lex_Luthor'.format(self.user_3.pk) + f'' + 'Lex_Luthor' ) self.assertEqual(user_username(self.user_3), expected) diff --git a/allianceauth/authentication/tests/test_app_settings.py b/allianceauth/authentication/tests/test_app_settings.py index 8fa9d368..5dd62930 100644 --- a/allianceauth/authentication/tests/test_app_settings.py +++ b/allianceauth/authentication/tests/test_app_settings.py @@ -1,4 +1,5 @@ from unittest.mock import Mock, patch + from django.test import TestCase from .. import app_settings @@ -83,7 +84,7 @@ class TestSetAppSetting(TestCase): self.assertEqual(result, 50) @patch(MODULE_PATH + '.app_settings.settings') - def test_default_for_invalid_type_int(self, mock_settings): + def test_default_for_outofrange_int(self, mock_settings): mock_settings.TEST_SETTING_DUMMY = 1000 result = app_settings._clean_setting( 'TEST_SETTING_DUMMY', @@ -96,7 +97,7 @@ class TestSetAppSetting(TestCase): def test_default_is_none_needs_required_type(self, mock_settings): mock_settings.TEST_SETTING_DUMMY = 'invalid type' with self.assertRaises(ValueError): - result = app_settings._clean_setting( + app_settings._clean_setting( 'TEST_SETTING_DUMMY', default_value=None ) diff --git a/allianceauth/authentication/tests/test_backend.py b/allianceauth/authentication/tests/test_backend.py index 92936355..d4969877 100644 --- a/allianceauth/authentication/tests/test_backend.py +++ b/allianceauth/authentication/tests/test_backend.py @@ -1,13 +1,13 @@ -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group, User from django.test import TestCase +from esi.models import Token + from allianceauth.eveonline.models import EveCharacter from allianceauth.tests.auth_utils import AuthUtils -from esi.models import Token - from ..backends import StateBackend -from ..models import CharacterOwnership, UserProfile, OwnershipRecord +from ..models import CharacterOwnership, OwnershipRecord, UserProfile MODULE_PATH = 'allianceauth.authentication' diff --git a/allianceauth/authentication/tests/test_decorators.py b/allianceauth/authentication/tests/test_decorators.py index 5b729d01..5a2fd39c 100644 --- a/allianceauth/authentication/tests/test_decorators.py +++ b/allianceauth/authentication/tests/test_decorators.py @@ -6,12 +6,11 @@ from django.contrib.auth.models import AnonymousUser from django.http.response import HttpResponse from django.test import TestCase from django.test.client import RequestFactory -from django.urls import reverse, URLPattern +from django.urls import URLPattern, reverse from allianceauth.eveonline.models import EveCharacter from allianceauth.tests.auth_utils import AuthUtils - from ..decorators import decorate_url_patterns, main_character_required from ..models import CharacterOwnership @@ -47,7 +46,7 @@ class DecoratorTestCase(TestCase): @mock.patch(MODULE_PATH + '.decorators.messages') def test_login_redirect(self, m): - setattr(self.request, 'user', AnonymousUser()) + self.request.user = AnonymousUser() response = self.dummy_view(self.request) self.assertEqual(response.status_code, 302) url = getattr(response, 'url', None) @@ -55,7 +54,7 @@ class DecoratorTestCase(TestCase): @mock.patch(MODULE_PATH + '.decorators.messages') def test_main_character_redirect(self, m): - setattr(self.request, 'user', self.no_main_user) + self.request.user = self.no_main_user response = self.dummy_view(self.request) self.assertEqual(response.status_code, 302) url = getattr(response, 'url', None) @@ -63,7 +62,7 @@ class DecoratorTestCase(TestCase): @mock.patch(MODULE_PATH + '.decorators.messages') def test_successful_request(self, m): - setattr(self.request, 'user', self.main_user) + self.request.user = self.main_user response = self.dummy_view(self.request) self.assertEqual(response.status_code, 200) diff --git a/allianceauth/authentication/tests/test_middleware.py b/allianceauth/authentication/tests/test_middleware.py index 70335e58..5f94abbd 100644 --- a/allianceauth/authentication/tests/test_middleware.py +++ b/allianceauth/authentication/tests/test_middleware.py @@ -1,10 +1,10 @@ -from unittest import mock -from allianceauth.authentication.middleware import UserSettingsMiddleware from unittest.mock import Mock -from django.http import HttpResponse +from django.http import HttpResponse from django.test.testcases import TestCase +from allianceauth.authentication.middleware import UserSettingsMiddleware + class TestUserSettingsMiddlewareSaveLang(TestCase): @@ -39,7 +39,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase): of a non-existent (anonymous) user """ self.request.user.is_anonymous = True - response = self.middleware.process_response( + self.middleware.process_response( self.request, self.response ) @@ -52,7 +52,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase): does the middleware change a language not set in the DB """ self.request.user.profile.language = None - response = self.middleware.process_response( + self.middleware.process_response( self.request, self.response ) @@ -64,7 +64,7 @@ class TestUserSettingsMiddlewareSaveLang(TestCase): """ Tests the middleware will change a language setting """ - response = self.middleware.process_response( + self.middleware.process_response( self.request, self.response ) @@ -158,7 +158,7 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase): tests the middleware will set night_mode if not set """ self.request.session = {} - response = self.middleware.process_response( + self.middleware.process_response( self.request, self.response ) @@ -168,7 +168,7 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase): """ tests the middleware will set night_mode if set. """ - response = self.middleware.process_response( + self.middleware.process_response( self.request, self.response ) diff --git a/allianceauth/authentication/tests/test_models.py b/allianceauth/authentication/tests/test_models.py index 4dca31ff..2f963708 100644 --- a/allianceauth/authentication/tests/test_models.py +++ b/allianceauth/authentication/tests/test_models.py @@ -3,12 +3,12 @@ from unittest import mock from django.contrib.auth.models import User from django.test import TestCase -from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ - EveAllianceInfo, EveFactionInfo -from allianceauth.tests.auth_utils import AuthUtils from esi.errors import IncompleteResponseError from esi.models import Token +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo +from allianceauth.tests.auth_utils import AuthUtils + from ..models import CharacterOwnership, State, get_guest_state from ..tasks import check_character_ownership diff --git a/allianceauth/authentication/tests/test_signals.py b/allianceauth/authentication/tests/test_signals.py index 8f7ba716..3336677e 100644 --- a/allianceauth/authentication/tests/test_signals.py +++ b/allianceauth/authentication/tests/test_signals.py @@ -1,19 +1,11 @@ + +from django.db.models.signals import post_save +from django.test.testcases import TestCase + from allianceauth.authentication.models import User, UserProfile -from allianceauth.eveonline.models import ( - EveCharacter, - EveCorporationInfo, - EveAllianceInfo -) -from django.db.models.signals import ( - pre_save, - post_save, - pre_delete, - m2m_changed -) +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from django.test.testcases import TestCase -from unittest.mock import Mock from . import patch diff --git a/allianceauth/authentication/tests/test_templatetags.py b/allianceauth/authentication/tests/test_templatetags.py index 450da33e..7f42f830 100644 --- a/allianceauth/authentication/tests/test_templatetags.py +++ b/allianceauth/authentication/tests/test_templatetags.py @@ -9,8 +9,12 @@ from django.core.cache import cache from django.test import TestCase from allianceauth.templatetags.admin_status import ( - _current_notifications, _current_version_summary, _fetch_list_from_gitlab, - _fetch_notification_issues_from_gitlab, _latests_versions, status_overview, + _current_notifications, + _current_version_summary, + _fetch_list_from_gitlab, + _fetch_notification_issues_from_gitlab, + _latests_versions, + status_overview, ) MODULE_PATH = 'allianceauth.templatetags' @@ -127,7 +131,7 @@ class TestNotifications(TestCase): # when result = _current_notifications() # then - self.assertEqual(result['notifications'], list()) + self.assertEqual(result['notifications'], []) @patch(MODULE_PATH + '.admin_status.cache') def test_current_notifications_is_none(self, mock_cache): @@ -136,7 +140,7 @@ class TestNotifications(TestCase): # when result = _current_notifications() # then - self.assertEqual(result['notifications'], list()) + self.assertEqual(result['notifications'], []) class TestCeleryQueueLength(TestCase): diff --git a/allianceauth/authentication/tests/test_views.py b/allianceauth/authentication/tests/test_views.py index ec7601a9..14394938 100644 --- a/allianceauth/authentication/tests/test_views.py +++ b/allianceauth/authentication/tests/test_views.py @@ -1,12 +1,13 @@ import json -import requests_mock from unittest.mock import patch +import requests_mock + from django.test import RequestFactory, TestCase -from allianceauth.authentication.views import task_counts, esi_check -from allianceauth.tests.auth_utils import AuthUtils from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES +from allianceauth.authentication.views import esi_check, task_counts +from allianceauth.tests.auth_utils import AuthUtils MODULE_PATH = "allianceauth.authentication.views" diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 2e8f4e2c..392a0a44 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -1,11 +1,6 @@ import logging import requests -from django_registration.backends.activation.views import ( - REGISTRATION_SALT, ActivationView as BaseActivationView, - RegistrationView as BaseRegistrationView, -) -from django_registration.signals import user_registered from django.conf import settings from django.contrib import messages @@ -18,6 +13,12 @@ from django.shortcuts import redirect, render from django.template.loader import render_to_string from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ +from django_registration.backends.activation.views import ( + REGISTRATION_SALT, + ActivationView as BaseActivationView, + RegistrationView as BaseRegistrationView, +) +from django_registration.signals import user_registered from esi.decorators import token_required from esi.models import Token @@ -32,7 +33,7 @@ from .models import CharacterOwnership if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS: _has_auto_groups = True - from allianceauth.eveonline.autogroups.models import * # noqa: F401, F403 + from allianceauth.eveonline.autogroups.models import * # noqa: F403 else: _has_auto_groups = False @@ -87,7 +88,7 @@ def dashboard_esi_check(request): @login_required def dashboard(request): - _dash_items = list() + _dash_items = [] hooks = get_hooks('dashboard_hook') items = [fn() for fn in hooks] items.sort(key=lambda i: i.order) @@ -164,9 +165,7 @@ def main_character_change(request, token): request.user.profile.save(update_fields=['main_character']) messages.success(request, _('Changed main character to %s') % co.character) logger.info( - 'Changed user {user} main character to {char}'.format( - user=request.user, char=co.character - ) + f'Changed user {request.user} main character to {co.character}' ) return redirect("authentication:dashboard") @@ -176,10 +175,9 @@ def add_character(request, token): if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter( owner_hash=token.character_owner_hash).filter(user=request.user).exists(): messages.success(request, _( - 'Added %(name)s to your account.' % ({'name': token.character_name}))) + 'Added {name} to your account.'.format(name=token.character_name))) else: - messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ( - {'name': token.character_name}))) + messages.error(request, _('Failed to add {name} to your account: they already have an account.'.format(name=token.character_name))) return redirect('authentication:dashboard') @@ -294,7 +292,7 @@ class RegistrationView(BaseRegistrationView): return redirect(settings.LOGIN_URL) if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): # Keep the request so the user can be automagically logged in. - setattr(self, 'request', request) + self.request = request return super().dispatch(request, *args, **kwargs) def register(self, form): diff --git a/allianceauth/bin/allianceauth.py b/allianceauth/bin/allianceauth.py index 20cfb53d..cd400526 100644 --- a/allianceauth/bin/allianceauth.py +++ b/allianceauth/bin/allianceauth.py @@ -2,6 +2,7 @@ import os import shutil from optparse import OptionParser + from django.core.management import call_command from django.core.management.commands.startproject import Command as BaseStartProject @@ -43,7 +44,7 @@ def create_project(parser, options, args): # Call the command with extra context call_command(StartProject(), *args, **command_options) - print(f"Success! {args[0]} has been created.") # noqa + print(f"Success! {args[0]} has been created.") def update_settings(parser, options, args): @@ -62,7 +63,7 @@ def update_settings(parser, options, args): # next check if given path is to the project, so the app is within it settings_path = os.path.join(project_path, project_name, 'settings/base.py') if not os.path.exists(settings_path): - parser.error("Unable to locate the Alliance Auth project at %s" % project_path) + parser.error(f"Unable to locate the Alliance Auth project at {project_path}") # first find the path to the Alliance Auth template settings import allianceauth diff --git a/allianceauth/checks.py b/allianceauth/checks.py index 704f6e6e..e6d0319d 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -1,12 +1,16 @@ -from django import db -from django.core.checks import CheckMessage, Error, register, Warning -from allianceauth.utils.cache import get_redis_client -from django.utils import timezone -from packaging.version import InvalidVersion, Version as Pep440Version -from celery import current_app -from django.conf import settings -from sqlite3.dbapi2 import sqlite_version_info import datetime +from sqlite3.dbapi2 import sqlite_version_info + +from celery import current_app +from packaging.version import InvalidVersion, Version as Pep440Version + +from django import db +from django.conf import settings +from django.core.checks import CheckMessage, Error, Warning, register +from django.utils import timezone + +from allianceauth.utils.cache import get_redis_client + """ A = System Packages B = Configuration @@ -101,8 +105,7 @@ def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011")) elif mariadb_version.major == 11 and mariadb_version.minor == 1: errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019")) - if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=datetime.timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012")) + errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012")) elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013")) @@ -168,7 +171,7 @@ def celery_settings(app_configs, **kwargs) -> list[CheckMessage]: errors.append(Error("Celery Priorities are not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003")) try: - if current_app.conf.broker_connection_retry_on_startup != True: + if current_app.conf.broker_connection_retry_on_startup is not True: errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004")) except KeyError: errors.append(Error("Celery broker_connection_retry_on_startup not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004")) diff --git a/allianceauth/context_processors.py b/allianceauth/context_processors.py index 4feb243e..cbc75a0e 100644 --- a/allianceauth/context_processors.py +++ b/allianceauth/context_processors.py @@ -1,4 +1,5 @@ from django.conf import settings + from .views import NightModeRedirectView diff --git a/allianceauth/corputils/admin.py b/allianceauth/corputils/admin.py index e0cdda66..c0a8de55 100644 --- a/allianceauth/corputils/admin.py +++ b/allianceauth/corputils/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import CorpStats, CorpMember +from .models import CorpMember, CorpStats admin.site.register(CorpStats) admin.site.register(CorpMember) diff --git a/allianceauth/corputils/auth_hooks.py b/allianceauth/corputils/auth_hooks.py index 4dd3e8ce..4414898c 100644 --- a/allianceauth/corputils/auth_hooks.py +++ b/allianceauth/corputils/auth_hooks.py @@ -1,8 +1,9 @@ -from allianceauth.menu.hooks import MenuItemHook -from allianceauth.services.hooks import UrlHook from django.utils.translation import gettext_lazy as _ + from allianceauth import hooks from allianceauth.corputils import urls +from allianceauth.menu.hooks import MenuItemHook +from allianceauth.services.hooks import UrlHook class CorpStats(MenuItemHook): diff --git a/allianceauth/corputils/managers.py b/allianceauth/corputils/managers.py index 87b2ffdd..f7bca4f9 100644 --- a/allianceauth/corputils/managers.py +++ b/allianceauth/corputils/managers.py @@ -1,6 +1,7 @@ -from django.db import models import logging +from django.db import models + logger = logging.getLogger(__name__) @@ -8,7 +9,7 @@ class CorpStatsQuerySet(models.QuerySet): def visible_to(self, user): # superusers get all visible if user.is_superuser: - logger.debug('Returning all corpstats for superuser %s.' % user) + logger.debug(f'Returning all corpstats for superuser {user}.') return self try: @@ -36,7 +37,7 @@ class CorpStatsQuerySet(models.QuerySet): query |= q return self.filter(query) except AssertionError: - logger.debug('User %s has no main character. No corpstats visible.' % user) + logger.debug(f'User {user} has no main character. No corpstats visible.') return self.none() diff --git a/allianceauth/corputils/migrations/0001_initial.py b/allianceauth/corputils/migrations/0001_initial.py index 49d71b75..e7439525 100644 --- a/allianceauth/corputils/migrations/0001_initial.py +++ b/allianceauth/corputils/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 1.10.1 on 2016-12-14 21:36 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/corputils/migrations/0002_migrate_permissions.py b/allianceauth/corputils/migrations/0002_migrate_permissions.py index fce155e4..cd75c9dc 100644 --- a/allianceauth/corputils/migrations/0002_migrate_permissions.py +++ b/allianceauth/corputils/migrations/0002_migrate_permissions.py @@ -66,7 +66,7 @@ def forward(apps, schema_editor): g.permissions.add(perm_dict['corpstats']['alliance_apis'].pk) g.permissions.add(perm_dict['corpstats']['view_alliance_corpstats'].pk) - for name, perm in perm_dict['user'].items(): + for _name, perm in perm_dict['user'].items(): perm.delete() diff --git a/allianceauth/corputils/migrations/0004_member_models.py b/allianceauth/corputils/migrations/0004_member_models.py index b8e2e291..8359669b 100644 --- a/allianceauth/corputils/migrations/0004_member_models.py +++ b/allianceauth/corputils/migrations/0004_member_models.py @@ -1,9 +1,10 @@ # Generated by Django 1.10.5 on 2017-03-26 20:13 -from django.db import migrations, models -import django.db.models.deletion import json +import django.db.models.deletion +from django.db import migrations, models + def convert_json_to_members(apps, schema_editor): CorpStats = apps.get_model('corputils', 'CorpStats') diff --git a/allianceauth/corputils/models.py b/allianceauth/corputils/models.py index 7ac03b7e..a98ec5cb 100644 --- a/allianceauth/corputils/models.py +++ b/allianceauth/corputils/models.py @@ -1,15 +1,17 @@ import logging import os -from allianceauth.authentication.models import CharacterOwnership, UserProfile from bravado.exception import HTTPForbidden + from django.db import models + from esi.errors import TokenError from esi.models import Token -from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter, EveAllianceInfo -from allianceauth.notifications import notify +from allianceauth.authentication.models import CharacterOwnership, UserProfile from allianceauth.corputils.managers import CorpStatsManager +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from allianceauth.notifications import notify SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') """ @@ -31,6 +33,7 @@ class CorpStats(models.Model): corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE) last_update = models.DateTimeField(auto_now=True) + objects = CorpStatsManager() class Meta: permissions = ( ('view_corp_corpstats', 'Can view corp stats of their corporation.'), @@ -40,7 +43,7 @@ class CorpStats(models.Model): verbose_name = "corp stats" verbose_name_plural = "corp stats" - objects = CorpStatsManager() + def __str__(self): return f"{self.__class__.__name__} for {self.corp}" @@ -76,21 +79,21 @@ class CorpStats(models.Model): logger.warning(f"{self} failed to update: {e}") if self.token.user: notify( - self.token.user, "%s failed to update with your ESI token." % self, + self.token.user, f"{self} failed to update with your ESI token.", message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.", level="error") self.delete() except HTTPForbidden as e: logger.warning(f"{self} failed to update: {e}") if self.token.user: - notify(self.token.user, "%s failed to update with your ESI token." % self, message=f"{e.status_code}: {e.message}", level="error") + notify(self.token.user, f"{self} failed to update with your ESI token.", message=f"{e.status_code}: {e.message}", level="error") self.delete() except AssertionError: - logger.warning("%s token character no longer in corp." % self) + logger.warning(f"{self} token character no longer in corp.") if self.token.user: notify( - self.token.user, "%s cannot update with your ESI token." % self, - message="%s cannot update with your ESI token as you have left corp." % self, level="error") + self.token.user, f"{self} cannot update with your ESI token.", + message=f"{self} cannot update with your ESI token as you have left corp.", level="error") self.delete() @property diff --git a/allianceauth/corputils/tasks.py b/allianceauth/corputils/tasks.py index 6cc016fa..fda7c5fb 100644 --- a/allianceauth/corputils/tasks.py +++ b/allianceauth/corputils/tasks.py @@ -1,4 +1,5 @@ from celery import shared_task + from allianceauth.corputils.models import CorpStats diff --git a/allianceauth/corputils/tests.py b/allianceauth/corputils/tests.py index 43e1f720..176541b3 100644 --- a/allianceauth/corputils/tests.py +++ b/allianceauth/corputils/tests.py @@ -1,14 +1,18 @@ from unittest import mock -from django.test import TestCase -from allianceauth.tests.auth_utils import AuthUtils -from .models import CorpStats, CorpMember -from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo, EveCharacter -from esi.models import Token -from esi.errors import TokenError from bravado.exception import HTTPForbidden + from django.contrib.auth.models import Permission +from django.test import TestCase + +from esi.errors import TokenError +from esi.models import Token + from allianceauth.authentication.models import CharacterOwnership +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from allianceauth.tests.auth_utils import AuthUtils + +from .models import CorpMember, CorpStats class CorpStatsManagerTestCase(TestCase): diff --git a/allianceauth/corputils/urls.py b/allianceauth/corputils/urls.py index 5f66c48e..7729d721 100644 --- a/allianceauth/corputils/urls.py +++ b/allianceauth/corputils/urls.py @@ -1,4 +1,5 @@ from django.urls import path + from . import views app_name = 'corputils' diff --git a/allianceauth/corputils/views.py b/allianceauth/corputils/views.py index 98367aef..d0e62690 100644 --- a/allianceauth/corputils/views.py +++ b/allianceauth/corputils/views.py @@ -1,16 +1,19 @@ import os from bravado.exception import HTTPError + from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required, user_passes_test -from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import IntegrityError -from django.shortcuts import render, redirect, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ + from esi.decorators import token_required + from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo -from .models import CorpStats, CorpMember +from .models import CorpMember, CorpStats SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') """ diff --git a/allianceauth/custom_css/admin.py b/allianceauth/custom_css/admin.py index 093e18e8..e7af932d 100644 --- a/allianceauth/custom_css/admin.py +++ b/allianceauth/custom_css/admin.py @@ -3,14 +3,15 @@ Admin classes for custom_css app """ # Django -from django.contrib import admin - # Django Solos from solo.admin import SingletonModelAdmin +from django.contrib import admin + +from allianceauth.custom_css.forms import CustomCSSAdminForm + # Alliance Auth Custom CSS from allianceauth.custom_css.models import CustomCSS -from allianceauth.custom_css.forms import CustomCSSAdminForm @admin.register(CustomCSS) diff --git a/allianceauth/custom_css/forms.py b/allianceauth/custom_css/forms.py index 6823b204..1b3f5955 100644 --- a/allianceauth/custom_css/forms.py +++ b/allianceauth/custom_css/forms.py @@ -3,12 +3,12 @@ Forms for custom_css app """ # Alliance Auth Custom CSS -from allianceauth.custom_css.models import CustomCSS -from allianceauth.custom_css.widgets import CssEditorWidget - # Django from django import forms +from allianceauth.custom_css.models import CustomCSS +from allianceauth.custom_css.widgets import CssEditorWidget + class CustomCSSAdminForm(forms.ModelForm): """ diff --git a/allianceauth/custom_css/models.py b/allianceauth/custom_css/models.py index c831cf6b..2b116fb4 100644 --- a/allianceauth/custom_css/models.py +++ b/allianceauth/custom_css/models.py @@ -21,7 +21,6 @@ class CustomCSS(SingletonModel): css = models.TextField( blank=True, - null=True, verbose_name=_("Your custom CSS"), help_text=_("This CSS will be added to the site after the default CSS."), ) diff --git a/allianceauth/custom_css/templatetags/custom_css.py b/allianceauth/custom_css/templatetags/custom_css.py index 3c01602f..1234c587 100644 --- a/allianceauth/custom_css/templatetags/custom_css.py +++ b/allianceauth/custom_css/templatetags/custom_css.py @@ -3,7 +3,7 @@ Custom template tags for custom_css app """ # Alliance Auth Custom CSS -from allianceauth.custom_css.models import CustomCSS +from pathlib import Path # Django from django.conf import settings @@ -11,7 +11,7 @@ from django.template.defaulttags import register from django.templatetags.static import static from django.utils.safestring import mark_safe -from pathlib import Path +from allianceauth.custom_css.models import CustomCSS @register.simple_tag diff --git a/allianceauth/custom_css/widgets.py b/allianceauth/custom_css/widgets.py index 32ed07dd..1c3a6290 100644 --- a/allianceauth/custom_css/widgets.py +++ b/allianceauth/custom_css/widgets.py @@ -6,7 +6,6 @@ Form widgets for custom_css app from django import forms # Alliance Auth -from allianceauth.custom_css.models import CustomCSS class CssEditorWidget(forms.Textarea): diff --git a/allianceauth/eveonline/admin.py b/allianceauth/eveonline/admin.py index e37e8aaa..69d428df 100644 --- a/allianceauth/eveonline/admin.py +++ b/allianceauth/eveonline/admin.py @@ -1,12 +1,9 @@ from django import forms from django.contrib import admin from django.core.exceptions import ObjectDoesNotExist -from .providers import ObjectNotFound -from .models import EveAllianceInfo -from .models import EveCharacter -from .models import EveCorporationInfo -from .models import EveFactionInfo +from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo +from .providers import ObjectNotFound class EveEntityExistsError(forms.ValidationError): diff --git a/allianceauth/eveonline/autogroups/admin.py b/allianceauth/eveonline/autogroups/admin.py index 240d4578..e202de92 100644 --- a/allianceauth/eveonline/autogroups/admin.py +++ b/allianceauth/eveonline/autogroups/admin.py @@ -1,9 +1,9 @@ -from django.contrib import admin -from django.db import models -from .models import AutogroupsConfig, ManagedCorpGroup, ManagedAllianceGroup - import logging +from django.contrib import admin +from django.db import models + +from .models import AutogroupsConfig, ManagedAllianceGroup, ManagedCorpGroup logger = logging.getLogger(__name__) diff --git a/allianceauth/eveonline/autogroups/apps.py b/allianceauth/eveonline/autogroups/apps.py index 69ca1b22..61d4bbe2 100644 --- a/allianceauth/eveonline/autogroups/apps.py +++ b/allianceauth/eveonline/autogroups/apps.py @@ -6,4 +6,4 @@ class EveAutogroupsConfig(AppConfig): label = 'eve_autogroups' def ready(self): - import allianceauth.eveonline.autogroups.signals + pass diff --git a/allianceauth/eveonline/autogroups/migrations/0001_initial.py b/allianceauth/eveonline/autogroups/migrations/0001_initial.py index 33c1ab42..9716d3af 100644 --- a/allianceauth/eveonline/autogroups/migrations/0001_initial.py +++ b/allianceauth/eveonline/autogroups/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.6 on 2017-12-23 04:30 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/eveonline/autogroups/models.py b/allianceauth/eveonline/autogroups/models.py index 88bc3ece..3f28f313 100644 --- a/allianceauth/eveonline/autogroups/models.py +++ b/allianceauth/eveonline/autogroups/models.py @@ -1,10 +1,11 @@ import logging -from django.db import models, transaction + from django.contrib.auth.models import Group, User from django.core.exceptions import ObjectDoesNotExist +from django.db import models, transaction from allianceauth.authentication.models import State -from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo +from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo logger = logging.getLogger(__name__) @@ -80,15 +81,15 @@ class AutogroupsConfig(models.Model): objects = AutogroupsConfigManager() + def __str__(self): + return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None)) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __repr__(self): return self.__class__.__name__ - def __str__(self): - return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None)) - def update_all_states_group_membership(self): list(map(self.update_group_membership_for_state, self.states.all())) @@ -235,7 +236,7 @@ class ManagedGroup(models.Model): abstract = True def __str__(self): - return "Managed Group: %s" % self.group.name + return f"Managed Group: {self.group.name}" class ManagedCorpGroup(ManagedGroup): corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE) diff --git a/allianceauth/eveonline/autogroups/signals.py b/allianceauth/eveonline/autogroups/signals.py index c2b8c11a..9b920d1b 100644 --- a/allianceauth/eveonline/autogroups/signals.py +++ b/allianceauth/eveonline/autogroups/signals.py @@ -1,7 +1,9 @@ import logging + +from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save from django.dispatch import receiver -from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed -from allianceauth.authentication.models import UserProfile, State + +from allianceauth.authentication.models import State, UserProfile from allianceauth.eveonline.models import EveCharacter from .models import AutogroupsConfig diff --git a/allianceauth/eveonline/autogroups/tests/__init__.py b/allianceauth/eveonline/autogroups/tests/__init__.py index cd1c4d68..8af31c2c 100644 --- a/allianceauth/eveonline/autogroups/tests/__init__.py +++ b/allianceauth/eveonline/autogroups/tests/__init__.py @@ -1,7 +1,10 @@ from unittest import mock -from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed + +from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save + from allianceauth.authentication.models import UserProfile from allianceauth.authentication.signals import reassess_on_profile_save + from .. import signals from ..models import AutogroupsConfig diff --git a/allianceauth/eveonline/autogroups/tests/test_managers.py b/allianceauth/eveonline/autogroups/tests/test_managers.py index e4e42ef0..e3afec8f 100644 --- a/allianceauth/eveonline/autogroups/tests/test_managers.py +++ b/allianceauth/eveonline/autogroups/tests/test_managers.py @@ -1,4 +1,5 @@ from django.test import TestCase + from allianceauth.tests.auth_utils import AuthUtils from ..models import AutogroupsConfig diff --git a/allianceauth/eveonline/autogroups/tests/test_models.py b/allianceauth/eveonline/autogroups/tests/test_models.py index f9c59389..7b6f8f89 100644 --- a/allianceauth/eveonline/autogroups/tests/test_models.py +++ b/allianceauth/eveonline/autogroups/tests/test_models.py @@ -1,15 +1,11 @@ -from django.test import TestCase from django.contrib.auth.models import Group -from django.db import transaction +from django.test import TestCase +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo - from ..models import AutogroupsConfig, get_users_for_state - - -from . import patch, connect_signals, disconnect_signals +from . import connect_signals, disconnect_signals, patch class AutogroupsConfigTestCase(TestCase): diff --git a/allianceauth/eveonline/autogroups/tests/test_signals.py b/allianceauth/eveonline/autogroups/tests/test_signals.py index 0ff3aa4c..6911b68c 100644 --- a/allianceauth/eveonline/autogroups/tests/test_signals.py +++ b/allianceauth/eveonline/autogroups/tests/test_signals.py @@ -1,13 +1,11 @@ -from django.test import TestCase from django.contrib.auth.models import User +from django.test import TestCase +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo - from ..models import AutogroupsConfig - -from . import patch, disconnect_signals, connect_signals +from . import connect_signals, disconnect_signals, patch class SignalsTestCase(TestCase): diff --git a/allianceauth/eveonline/evelinks/dotlan.py b/allianceauth/eveonline/evelinks/dotlan.py index b782eabd..b067b338 100644 --- a/allianceauth/eveonline/evelinks/dotlan.py +++ b/allianceauth/eveonline/evelinks/dotlan.py @@ -1,14 +1,8 @@ # this module generates profile URLs for dotlan -from urllib.parse import urljoin, quote - -from . import ( - _ESI_CATEGORY_ALLIANCE, - _ESI_CATEGORY_CORPORATION, - _ESI_CATEGORY_REGION, - _ESI_CATEGORY_SOLARSYSTEM -) +from urllib.parse import quote, urljoin +from . import _ESI_CATEGORY_ALLIANCE, _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_REGION, _ESI_CATEGORY_SOLARSYSTEM _BASE_URL = 'http://evemaps.dotlan.net' diff --git a/allianceauth/eveonline/evelinks/eveimageserver.py b/allianceauth/eveonline/evelinks/eveimageserver.py index 172c1bbc..aa0570a2 100644 --- a/allianceauth/eveonline/evelinks/eveimageserver.py +++ b/allianceauth/eveonline/evelinks/eveimageserver.py @@ -1,10 +1,4 @@ -from . import ( - _ESI_CATEGORY_ALLIANCE, - _ESI_CATEGORY_CHARACTER, - _ESI_CATEGORY_CORPORATION, - _ESI_CATEGORY_INVENTORYTYPE -) - +from . import _ESI_CATEGORY_ALLIANCE, _ESI_CATEGORY_CHARACTER, _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_INVENTORYTYPE _EVE_IMAGE_SERVER_URL = 'https://images.evetech.net' _DEFAULT_IMAGE_SIZE = 32 @@ -70,10 +64,7 @@ def _eve_entity_image_url( if variant: if variant not in categories[category]['variants']: - raise ValueError('Invalid variant {} for category {}'.format( - variant, - category - )) + raise ValueError(f'Invalid variant {variant} for category {category}') else: variant = categories[category]['variants'][0] @@ -81,13 +72,7 @@ def _eve_entity_image_url( raise ValueError(f'Invalid tenant {tenant}') # compose result URL - result = '{}/{}/{}/{}?size={}'.format( - _EVE_IMAGE_SERVER_URL, - endpoint, - entity_id, - variant, - size - ) + result = f'{_EVE_IMAGE_SERVER_URL}/{endpoint}/{entity_id}/{variant}?size={size}' if tenant: result += f'&tenant={tenant}' diff --git a/allianceauth/eveonline/evelinks/evewho.py b/allianceauth/eveonline/evelinks/evewho.py index 6fd05b14..52385953 100644 --- a/allianceauth/eveonline/evelinks/evewho.py +++ b/allianceauth/eveonline/evelinks/evewho.py @@ -4,11 +4,10 @@ from urllib.parse import urljoin from . import ( _ESI_CATEGORY_ALLIANCE, - _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_CHARACTER, + _ESI_CATEGORY_CORPORATION, ) - _BASE_URL = 'https://evewho.com' diff --git a/allianceauth/eveonline/evelinks/tests/test_evelinks.py b/allianceauth/eveonline/evelinks/tests/test_evelinks.py index 32f79f05..955854cf 100644 --- a/allianceauth/eveonline/evelinks/tests/test_evelinks.py +++ b/allianceauth/eveonline/evelinks/tests/test_evelinks.py @@ -1,8 +1,6 @@ from django.test import TestCase -from ...models import EveCharacter, EveCorporationInfo, EveAllianceInfo -from .. import dotlan, zkillboard, evewho, eveimageserver -from ...templatetags import evelinks +from .. import dotlan, eveimageserver, evewho, zkillboard class TestEveWho(TestCase): diff --git a/allianceauth/eveonline/evelinks/tests/test_templatetags.py b/allianceauth/eveonline/evelinks/tests/test_templatetags.py index db5c8178..2e96f278 100644 --- a/allianceauth/eveonline/evelinks/tests/test_templatetags.py +++ b/allianceauth/eveonline/evelinks/tests/test_templatetags.py @@ -1,8 +1,8 @@ from django.test import TestCase -from ...models import EveCharacter, EveCorporationInfo, EveAllianceInfo -from .. import eveimageserver, evewho, dotlan, zkillboard +from ...models import EveAllianceInfo, EveCharacter, EveCorporationInfo from ...templatetags import evelinks +from .. import dotlan, eveimageserver, evewho, zkillboard class TestTemplateTags(TestCase): diff --git a/allianceauth/eveonline/evelinks/zkillboard.py b/allianceauth/eveonline/evelinks/zkillboard.py index 3bba9833..4ce84bb2 100644 --- a/allianceauth/eveonline/evelinks/zkillboard.py +++ b/allianceauth/eveonline/evelinks/zkillboard.py @@ -4,13 +4,12 @@ from urllib.parse import urljoin from . import ( _ESI_CATEGORY_ALLIANCE, - _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_CHARACTER, + _ESI_CATEGORY_CORPORATION, _ESI_CATEGORY_REGION, - _ESI_CATEGORY_SOLARSYSTEM + _ESI_CATEGORY_SOLARSYSTEM, ) - _BASE_URL = 'https://zkillboard.com' diff --git a/allianceauth/eveonline/managers.py b/allianceauth/eveonline/managers.py index 5e849edc..eff9b31e 100644 --- a/allianceauth/eveonline/managers.py +++ b/allianceauth/eveonline/managers.py @@ -1,6 +1,7 @@ import logging from django.db import models + from . import providers logger = logging.getLogger(__name__) diff --git a/allianceauth/eveonline/migrations/0001_initial.py b/allianceauth/eveonline/migrations/0001_initial.py index a647ca0b..d45d329c 100644 --- a/allianceauth/eveonline/migrations/0001_initial.py +++ b/allianceauth/eveonline/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:39 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/eveonline/migrations/0003_auto_20161026_0149.py b/allianceauth/eveonline/migrations/0003_auto_20161026_0149.py index 0dda48fa..9cfdef65 100644 --- a/allianceauth/eveonline/migrations/0003_auto_20161026_0149.py +++ b/allianceauth/eveonline/migrations/0003_auto_20161026_0149.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-10-26 01:49 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/eveonline/migrations/0009_on_delete.py b/allianceauth/eveonline/migrations/0009_on_delete.py index d47b068b..668ca3da 100644 --- a/allianceauth/eveonline/migrations/0009_on_delete.py +++ b/allianceauth/eveonline/migrations/0009_on_delete.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.5 on 2017-09-28 02:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/eveonline/models.py b/allianceauth/eveonline/models.py index 85993cbd..6c9e621d 100644 --- a/allianceauth/eveonline/models.py +++ b/allianceauth/eveonline/models.py @@ -1,8 +1,8 @@ import logging -from typing import Union from django.core.exceptions import ObjectDoesNotExist from django.db import models + from esi.models import Token from allianceauth.notifications import notify @@ -80,7 +80,8 @@ class EveAllianceInfo(models.Model): class Meta: indexes = [models.Index(fields=['executor_corp_id',])] - + def __str__(self): + return self.alliance_name def populate_alliance(self): alliance = self.provider.get_alliance(self.alliance_id) for corp_id in alliance.corp_ids: @@ -100,8 +101,7 @@ class EveAllianceInfo(models.Model): self.save() return self - def __str__(self): - return self.alliance_name + @staticmethod def generic_logo_url( @@ -152,7 +152,8 @@ class EveCorporationInfo(models.Model): class Meta: indexes = [models.Index(fields=['ceo_id',]),] - + def __str__(self): + return self.corporation_name def update_corporation(self, corp: providers.Corporation = None): if corp is None: corp = self.provider.get_corporation(self.corporation_id) @@ -165,8 +166,7 @@ class EveCorporationInfo(models.Model): self.save() return self - def __str__(self): - return self.corporation_name + @staticmethod def generic_logo_url( @@ -209,10 +209,10 @@ class EveCharacter(models.Model): corporation_name = models.CharField(max_length=254) corporation_ticker = models.CharField(max_length=5) alliance_id = models.PositiveIntegerField(blank=True, null=True, default=None) - alliance_name = models.CharField(max_length=254, blank=True, null=True, default='') - alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='') - faction_id = models.PositiveIntegerField(blank=True, null=True, default=None) - faction_name = models.CharField(max_length=254, blank=True, null=True, default='') + alliance_name = models.CharField(max_length=254, blank=True, default='') + alliance_ticker = models.CharField(max_length=5, blank=True, default='') + faction_id = models.PositiveIntegerField(blank=True, default=None) + faction_name = models.CharField(max_length=254, blank=True, default='') objects = EveCharacterManager() provider = EveCharacterProviderManager() diff --git a/allianceauth/eveonline/providers.py b/allianceauth/eveonline/providers.py index d8c30604..9ba2db92 100644 --- a/allianceauth/eveonline/providers.py +++ b/allianceauth/eveonline/providers.py @@ -1,16 +1,16 @@ import logging import os -from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity, HTTPError +from bravado.exception import HTTPError, HTTPNotFound, HTTPUnprocessableEntity from jsonschema.exceptions import RefResolutionError from django.conf import settings + from esi.clients import esi_client_factory from allianceauth import __version__ from allianceauth.utils.django import StartupCommand - SWAGGER_SPEC_PATH = os.path.join(os.path.dirname( os.path.abspath(__file__)), 'swagger.json' ) diff --git a/allianceauth/eveonline/tasks.py b/allianceauth/eveonline/tasks.py index ce2b4219..e753ab93 100644 --- a/allianceauth/eveonline/tasks.py +++ b/allianceauth/eveonline/tasks.py @@ -2,9 +2,8 @@ import logging from celery import shared_task -from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo from . import providers - +from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo logger = logging.getLogger(__name__) diff --git a/allianceauth/eveonline/templatetags/evelinks.py b/allianceauth/eveonline/templatetags/evelinks.py index e5f44440..4ca6b022 100644 --- a/allianceauth/eveonline/templatetags/evelinks.py +++ b/allianceauth/eveonline/templatetags/evelinks.py @@ -14,8 +14,8 @@ from django import template -from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo -from ..evelinks import eveimageserver, evewho, dotlan, zkillboard +from ..evelinks import dotlan, eveimageserver, evewho, zkillboard +from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo register = template.Library() @@ -30,7 +30,7 @@ def _generic_character_url( eve_obj: EveCharacter ) -> str: """returns character URL for given provider and object""" - my_func = getattr(provider, 'character_url') + my_func = provider.character_url if isinstance(eve_obj, EveCharacter): return my_func(getattr(eve_obj, obj_prop)) @@ -47,8 +47,8 @@ def _generic_corporation_url( eve_obj: object ) -> str: """returns corporation URL for given provider and object""" - my_func = getattr(provider, 'corporation_url') - if isinstance(eve_obj, (EveCharacter, EveCorporationInfo)): + my_func = provider.corporation_url + if isinstance(eve_obj, EveCharacter | EveCorporationInfo): return my_func(getattr(eve_obj, obj_prop)) elif eve_obj is None: @@ -64,7 +64,7 @@ def _generic_alliance_url( eve_obj: object ) -> str: """returns alliance URL for given provider and object""" - my_func = getattr(provider, 'alliance_url') + my_func = provider.alliance_url if isinstance(eve_obj, EveCharacter): if eve_obj.alliance_id: diff --git a/allianceauth/eveonline/tests/esi_client_stub.py b/allianceauth/eveonline/tests/esi_client_stub.py index 1eadfbd3..9382355c 100644 --- a/allianceauth/eveonline/tests/esi_client_stub.py +++ b/allianceauth/eveonline/tests/esi_client_stub.py @@ -10,7 +10,7 @@ class BravadoResponseStub: self.reason = reason self.status_code = status_code self.text = text - self.headers = headers if headers else dict() + self.headers = headers if headers else {} self.raw_bytes = raw_bytes def __str__(self): diff --git a/allianceauth/eveonline/tests/test_managers.py b/allianceauth/eveonline/tests/test_managers.py index 5bab0771..61512ae5 100644 --- a/allianceauth/eveonline/tests/test_managers.py +++ b/allianceauth/eveonline/tests/test_managers.py @@ -2,8 +2,8 @@ from unittest import mock from django.test import TestCase -from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo -from ..providers import Character, Corporation, Alliance +from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from ..providers import Alliance, Character, Corporation class EveCharacterProviderManagerTestCase(TestCase): @@ -58,7 +58,7 @@ class EveCharacterManagerTestCase(TestCase): @mock.patch('allianceauth.eveonline.managers.providers.provider') def test_update_character(self, provider): # Also covers Model.update_character - existing = EveCharacter.objects.create( + EveCharacter.objects.create( character_id=1234, character_name='character.name', corporation_id=23457, diff --git a/allianceauth/eveonline/tests/test_models.py b/allianceauth/eveonline/tests/test_models.py index 94c9614a..e4124718 100644 --- a/allianceauth/eveonline/tests/test_models.py +++ b/allianceauth/eveonline/tests/test_models.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase + from esi.models import Token from allianceauth.tests.auth_utils import AuthUtils @@ -62,7 +63,7 @@ class EveCharacterTestCase(TestCase): ) with self.assertRaises(EveCorporationInfo.DoesNotExist): - character.corporation + _ = character.corporation def test_alliance_prop(self): """ @@ -111,7 +112,7 @@ class EveCharacterTestCase(TestCase): ) with self.assertRaises(EveAllianceInfo.DoesNotExist): - character.alliance + _ = character.alliance def test_alliance_prop_none(self): """ @@ -169,7 +170,7 @@ class EveCharacterTestCase(TestCase): ) with self.assertRaises(EveFactionInfo.DoesNotExist): - character.faction + _ = character.faction def test_faction_prop_none(self): """ diff --git a/allianceauth/eveonline/tests/test_providers.py b/allianceauth/eveonline/tests/test_providers.py index 95f4f069..3110979f 100644 --- a/allianceauth/eveonline/tests/test_providers.py +++ b/allianceauth/eveonline/tests/test_providers.py @@ -6,20 +6,20 @@ from jsonschema.exceptions import RefResolutionError from django.test import TestCase -from . import set_logger -from .esi_client_stub import EsiClientStub from ..providers import ( - ObjectNotFound, - Entity, + Alliance, AllianceMixin, - FactionMixin, Character, Corporation, - Alliance, - ItemType, + Entity, EveProvider, - EveSwaggerProvider + EveSwaggerProvider, + FactionMixin, + ItemType, + ObjectNotFound, ) +from . import set_logger +from .esi_client_stub import EsiClientStub MODULE_PATH = 'allianceauth.eveonline.providers' SWAGGER_OLD_SPEC_PATH = os.path.join(os.path.dirname( @@ -544,7 +544,7 @@ class TestEveSwaggerProvider(TestCase): } mock_result = Mock() if isinstance(characters, list): - characters_result = list() + characters_result = [] for character_id in characters: if character_id in character_data: characters_result.append(character_data[character_id]) diff --git a/allianceauth/fleetactivitytracking/admin.py b/allianceauth/fleetactivitytracking/admin.py index 6ac8ca30..88d3d8f4 100644 --- a/allianceauth/fleetactivitytracking/admin.py +++ b/allianceauth/fleetactivitytracking/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from allianceauth.fleetactivitytracking.models import Fatlink, Fat +from allianceauth.fleetactivitytracking.models import Fat, Fatlink admin.site.register(Fatlink) admin.site.register(Fat) diff --git a/allianceauth/fleetactivitytracking/auth_hooks.py b/allianceauth/fleetactivitytracking/auth_hooks.py index fc5821b2..57b0362d 100644 --- a/allianceauth/fleetactivitytracking/auth_hooks.py +++ b/allianceauth/fleetactivitytracking/auth_hooks.py @@ -1,9 +1,11 @@ -from allianceauth.menu.hooks import MenuItemHook -from . import urls from django.utils.translation import gettext_lazy as _ + from allianceauth import hooks +from allianceauth.menu.hooks import MenuItemHook from allianceauth.services.hooks import UrlHook +from . import urls + @hooks.register('menu_item_hook') def register_menu(): diff --git a/allianceauth/fleetactivitytracking/migrations/0002_auto_20160905_2220.py b/allianceauth/fleetactivitytracking/migrations/0002_auto_20160905_2220.py index 813a7f84..5241d390 100644 --- a/allianceauth/fleetactivitytracking/migrations/0002_auto_20160905_2220.py +++ b/allianceauth/fleetactivitytracking/migrations/0002_auto_20160905_2220.py @@ -1,6 +1,7 @@ # Generated by Django 1.10.1 on 2016-09-05 22:20 import datetime + from django.db import migrations, models diff --git a/allianceauth/fleetactivitytracking/migrations/0003_auto_20160906_2354.py b/allianceauth/fleetactivitytracking/migrations/0003_auto_20160906_2354.py index 435f0a9a..cc14b7ea 100644 --- a/allianceauth/fleetactivitytracking/migrations/0003_auto_20160906_2354.py +++ b/allianceauth/fleetactivitytracking/migrations/0003_auto_20160906_2354.py @@ -1,7 +1,7 @@ # Generated by Django 1.10.1 on 2016-09-06 23:54 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/fleetactivitytracking/models.py b/allianceauth/fleetactivitytracking/models.py index 3cbd3d63..b4041efc 100644 --- a/allianceauth/fleetactivitytracking/models.py +++ b/allianceauth/fleetactivitytracking/models.py @@ -29,4 +29,4 @@ class Fat(models.Model): unique_together = (('character', 'fatlink'),) def __str__(self): - return "Fat-link for %s" % self.character.character_name + return f"Fat-link for {self.character.character_name}" diff --git a/allianceauth/fleetactivitytracking/urls.py b/allianceauth/fleetactivitytracking/urls.py index 41603150..e38f9154 100644 --- a/allianceauth/fleetactivitytracking/urls.py +++ b/allianceauth/fleetactivitytracking/urls.py @@ -1,6 +1,7 @@ from django.urls import path from . import views + app_name = 'fleetactivitytracking' urlpatterns = [ diff --git a/allianceauth/fleetactivitytracking/views.py b/allianceauth/fleetactivitytracking/views.py index 232fa456..d7c3ee07 100644 --- a/allianceauth/fleetactivitytracking/views.py +++ b/allianceauth/fleetactivitytracking/views.py @@ -2,24 +2,23 @@ import datetime import logging import os -from allianceauth.authentication.models import CharacterOwnership from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User -from django.core.exceptions import ValidationError, ObjectDoesNotExist -from django.shortcuts import render, redirect, get_object_or_404, Http404 +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone -from django.utils.translation import gettext_lazy as _ -from esi.decorators import token_required -from allianceauth.eveonline.providers import provider -from .forms import FatlinkForm -from .models import Fatlink, Fat from django.utils.crypto import get_random_string +from django.utils.translation import gettext_lazy as _ -from allianceauth.eveonline.models import EveAllianceInfo -from allianceauth.eveonline.models import EveCharacter -from allianceauth.eveonline.models import EveCorporationInfo +from esi.decorators import token_required + +from allianceauth.authentication.models import CharacterOwnership +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo +from allianceauth.eveonline.providers import provider + +from .forms import FatlinkForm +from .models import Fat, Fatlink SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') """ @@ -50,7 +49,7 @@ class CorpStat: try: return "%.2f" % (float(self.n_fats) / float(self.corp.member_count)) except ZeroDivisionError: - return "%.2f" % 0 + return f"{0:.2f}" class MemberStat: @@ -72,7 +71,7 @@ class MemberStat: try: return "%.2f" % (float(self.n_fats) / float(self.n_chars)) except ZeroDivisionError: - return "%.2f" % 0 + return f"{0:.2f}" def first_day_of_next_month(year, month): @@ -95,7 +94,7 @@ def fatlink_view(request): # If the user has the right privileges the site will also show the latest fatlinks with the options to add VIPs and # manually add players. user = request.user - logger.debug("fatlink_view called by user %s" % request.user) + logger.debug(f"fatlink_view called by user {request.user}") latest_fats = Fat.objects.select_related('character', 'fatlink').filter(user=user).order_by('-id')[:5] if user.has_perm('auth.fleetactivitytracking'): @@ -182,7 +181,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year): logger.debug("Personal statistics view for year %i called by %s" % (year, request.user)) user = request.user - logger.debug("fatlink_personal_statistics_view called by user %s" % request.user) + logger.debug(f"fatlink_personal_statistics_view called by user {request.user}") personal_fats = Fat.objects.select_related('fatlink').filter(user=user).order_by('id') @@ -227,7 +226,7 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None) personal_fats = Fat.objects.filter(user=user)\ .filter(fatlink__fatdatetime__gte=start_of_month).filter(fatlink__fatdatetime__lt=start_of_next_month) - ship_statistics = dict() + ship_statistics = {} n_fats = 0 for fat in personal_fats: ship_statistics[fat.shiptype] = ship_statistics.setdefault(fat.shiptype, 0) + 1 @@ -300,7 +299,7 @@ def click_fatlink_view(request, token, fat_hash=None): except ValidationError as e: err_messages = [] - for errorname, message in e.message_dict.items(): + for _errorname, message in e.message_dict.items(): err_messages.append(message[0]) messages.error(request, ' '.join(err_messages)) @@ -330,12 +329,12 @@ def click_fatlink_view(request, token, fat_hash=None): @login_required @permission_required('auth.fleetactivitytracking') def create_fatlink_view(request): - logger.debug("create_fatlink_view called by user %s" % request.user) + logger.debug(f"create_fatlink_view called by user {request.user}") if request.method == 'POST': - logger.debug("Post request to create_fatlink_view by user %s" % request.user) + logger.debug(f"Post request to create_fatlink_view by user {request.user}") form = FatlinkForm(request.POST) if 'submit_fat' in request.POST: - logger.debug("Submitting fleetactivitytracking by user %s" % request.user) + logger.debug(f"Submitting fleetactivitytracking by user {request.user}") if form.is_valid(): fatlink = Fatlink() fatlink.fleet = form.cleaned_data["fleet"] @@ -349,7 +348,7 @@ def create_fatlink_view(request): except ValidationError as e: form = FatlinkForm() messages = [] - for errorname, message in e.message_dict.items(): + for _errorname, message in e.message_dict.items(): messages.append(message[0].decode()) context = {'form': form, 'errormessages': messages} return render(request, 'fleetactivitytracking/fatlinkcreate.html', context=context) @@ -361,7 +360,7 @@ def create_fatlink_view(request): else: form = FatlinkForm() - logger.debug("Returning empty form to user %s" % request.user) + logger.debug(f"Returning empty form to user {request.user}") context = {'form': form} @@ -371,7 +370,7 @@ def create_fatlink_view(request): @login_required @permission_required('auth.fleetactivitytracking') def modify_fatlink_view(request, fat_hash=None): - logger.debug("modify_fatlink_view called by user %s" % request.user) + logger.debug(f"modify_fatlink_view called by user {request.user}") fatlink = get_object_or_404(Fatlink, hash=fat_hash) if request.GET.get('removechar', None): @@ -382,7 +381,7 @@ def modify_fatlink_view(request, fat_hash=None): Fat.objects.filter(fatlink=fatlink).filter(character=character).delete() if request.GET.get('deletefat', None): - logger.debug("Removing fleetactivitytracking %s" % fatlink) + logger.debug(f"Removing fleetactivitytracking {fatlink}") fatlink.delete() return redirect('fatlink:view') diff --git a/allianceauth/framework/api/evecharacter.py b/allianceauth/framework/api/evecharacter.py index d6a77f05..539f326b 100644 --- a/allianceauth/framework/api/evecharacter.py +++ b/allianceauth/framework/api/evecharacter.py @@ -2,7 +2,6 @@ Alliance Auth Evecharacter API """ -from typing import Optional from django.contrib.auth.models import User diff --git a/allianceauth/framework/api/user.py b/allianceauth/framework/api/user.py index e1597e72..1be7fbd4 100644 --- a/allianceauth/framework/api/user.py +++ b/allianceauth/framework/api/user.py @@ -2,7 +2,6 @@ Alliance Auth User API """ -from typing import Optional from django.contrib.auth.models import User diff --git a/allianceauth/framework/tests/test_api_user.py b/allianceauth/framework/tests/test_api_user.py index 1b43b75c..47398063 100644 --- a/allianceauth/framework/tests/test_api_user.py +++ b/allianceauth/framework/tests/test_api_user.py @@ -10,9 +10,9 @@ from django.test import TestCase # Alliance Auth from allianceauth.framework.api.user import ( - get_sentinel_user, get_main_character_from_user, - get_main_character_name_from_user + get_main_character_name_from_user, + get_sentinel_user, ) from allianceauth.tests.auth_utils import AuthUtils diff --git a/allianceauth/groupmanagement/admin.py b/allianceauth/groupmanagement/admin.py index d51cf7d2..90d83506 100644 --- a/allianceauth/groupmanagement/admin.py +++ b/allianceauth/groupmanagement/admin.py @@ -1,16 +1,9 @@ from django.apps import apps from django.contrib import admin - from django.contrib.auth.models import Group as BaseGroup, Permission, User from django.db.models import Count, Exists, OuterRef from django.db.models.functions import Lower -from django.db.models.signals import ( - m2m_changed, - post_delete, - post_save, - pre_delete, - pre_save -) +from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save from django.dispatch import receiver from .forms import GroupAdminForm, ReservedGroupNameAdminForm @@ -172,7 +165,7 @@ class GroupAdmin(admin.ModelAdmin): return obj.has_leader or obj.has_leader_groups def _properties(self, obj): - properties = list() + properties = [] if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance): properties.append('Auto Group') elif obj.authgroup.internal: diff --git a/allianceauth/groupmanagement/auth_hooks.py b/allianceauth/groupmanagement/auth_hooks.py index 318d2095..1d46e64e 100644 --- a/allianceauth/groupmanagement/auth_hooks.py +++ b/allianceauth/groupmanagement/auth_hooks.py @@ -1,8 +1,8 @@ from django.utils.translation import gettext_lazy as _ -from allianceauth.menu.hooks import MenuItemHook -from allianceauth.services.hooks import UrlHook from allianceauth import hooks +from allianceauth.menu.hooks import MenuItemHook +from allianceauth.services.hooks import UrlHook from . import urls from .managers import GroupManager diff --git a/allianceauth/groupmanagement/managers.py b/allianceauth/groupmanagement/managers.py index ad8db92e..93e7789f 100644 --- a/allianceauth/groupmanagement/managers.py +++ b/allianceauth/groupmanagement/managers.py @@ -4,8 +4,8 @@ from django.contrib.auth.models import Group, User from django.db.models import Q, QuerySet from allianceauth.authentication.models import State -from .models import GroupRequest +from .models import GroupRequest logger = logging.getLogger(__name__) diff --git a/allianceauth/groupmanagement/migrations/0001_initial.py b/allianceauth/groupmanagement/migrations/0001_initial.py index c6112559..00ca959b 100644 --- a/allianceauth/groupmanagement/migrations/0001_initial.py +++ b/allianceauth/groupmanagement/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:39 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/groupmanagement/migrations/0002_auto_20160906_2354.py b/allianceauth/groupmanagement/migrations/0002_auto_20160906_2354.py index 76cef366..74574d76 100644 --- a/allianceauth/groupmanagement/migrations/0002_auto_20160906_2354.py +++ b/allianceauth/groupmanagement/migrations/0002_auto_20160906_2354.py @@ -1,7 +1,7 @@ # Generated by Django 1.10.1 on 2016-09-06 23:54 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/groupmanagement/migrations/0003_default_groups.py b/allianceauth/groupmanagement/migrations/0003_default_groups.py index afe1bed0..84472518 100644 --- a/allianceauth/groupmanagement/migrations/0003_default_groups.py +++ b/allianceauth/groupmanagement/migrations/0003_default_groups.py @@ -1,7 +1,7 @@ # Generated by Django 1.10.1 on 2016-09-09 23:22 from django.db import migrations -from django.conf import settings + class Migration(migrations.Migration): diff --git a/allianceauth/groupmanagement/migrations/0004_authgroup.py b/allianceauth/groupmanagement/migrations/0004_authgroup.py index e3c8848e..9aacb71f 100644 --- a/allianceauth/groupmanagement/migrations/0004_authgroup.py +++ b/allianceauth/groupmanagement/migrations/0004_authgroup.py @@ -1,9 +1,9 @@ # Generated by Django 1.10.2 on 2016-12-04 10:25 -from django.conf import settings -from django.db import migrations, models -from django.core.exceptions import ObjectDoesNotExist import django.db.models.deletion +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations, models def internal_group(group): diff --git a/allianceauth/groupmanagement/migrations/0006_request_groups_perm.py b/allianceauth/groupmanagement/migrations/0006_request_groups_perm.py index 69544c9c..83a4a554 100644 --- a/allianceauth/groupmanagement/migrations/0006_request_groups_perm.py +++ b/allianceauth/groupmanagement/migrations/0006_request_groups_perm.py @@ -1,12 +1,9 @@ # Generated by Django 1.10.5 on 2017-02-04 07:17 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/groupmanagement/migrations/0009_requestlog.py b/allianceauth/groupmanagement/migrations/0009_requestlog.py index aaff4927..d81d4ae4 100644 --- a/allianceauth/groupmanagement/migrations/0009_requestlog.py +++ b/allianceauth/groupmanagement/migrations/0009_requestlog.py @@ -1,8 +1,8 @@ # Generated by Django 2.0.6 on 2018-06-04 02:45 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/groupmanagement/migrations/0011_requestlog_date.py b/allianceauth/groupmanagement/migrations/0011_requestlog_date.py index fbe535ce..23f8157c 100644 --- a/allianceauth/groupmanagement/migrations/0011_requestlog_date.py +++ b/allianceauth/groupmanagement/migrations/0011_requestlog_date.py @@ -1,6 +1,7 @@ # Generated by Django 2.0.8 on 2018-12-07 08:56 import datetime + from django.db import migrations, models diff --git a/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py b/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py index 0349b9e2..0d0aa83e 100644 --- a/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py +++ b/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.9 on 2021-11-25 18:38 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/groupmanagement/models.py b/allianceauth/groupmanagement/models.py index 19e15c72..23bba04b 100644 --- a/allianceauth/groupmanagement/models.py +++ b/allianceauth/groupmanagement/models.py @@ -1,4 +1,4 @@ -from typing import Set + from django.conf import settings from django.contrib.auth.models import Group, User @@ -52,6 +52,9 @@ class RequestLog(models.Model): request_actor = models.ForeignKey(User, on_delete=models.CASCADE) date = models.DateTimeField(auto_now_add=True) + def __str__(self): + return self.pk + def requestor(self): return self.request_info.split(":")[0] @@ -176,7 +179,7 @@ class AuthGroup(models.Model): permissions = ( ("request_groups", _("Can request non-public groups")), ) - default_permissions = tuple() + default_permissions = () def __str__(self): return self.group.name diff --git a/allianceauth/groupmanagement/signals.py b/allianceauth/groupmanagement/signals.py index 60bbeb80..951e9980 100644 --- a/allianceauth/groupmanagement/signals.py +++ b/allianceauth/groupmanagement/signals.py @@ -1,6 +1,7 @@ import logging + from django.contrib.auth.models import Group -from django.db.models.signals import pre_save, post_save +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver from allianceauth.authentication.signals import state_changed diff --git a/allianceauth/groupmanagement/tests/__init__.py b/allianceauth/groupmanagement/tests/__init__.py index 1446ef49..aa4816f5 100644 --- a/allianceauth/groupmanagement/tests/__init__.py +++ b/allianceauth/groupmanagement/tests/__init__.py @@ -3,9 +3,6 @@ from django.urls import reverse def get_admin_change_view_url(obj: object) -> str: return reverse( - 'admin:{}_{}_change'.format( - obj._meta.app_label, - type(obj).__name__.lower() - ), + f'admin:{obj._meta.app_label}_{type(obj).__name__.lower()}_change', args=(obj.pk,) ) diff --git a/allianceauth/groupmanagement/tests/test_admin.py b/allianceauth/groupmanagement/tests/test_admin.py index 3b9605a1..0f7a1ed1 100644 --- a/allianceauth/groupmanagement/tests/test_admin.py +++ b/allianceauth/groupmanagement/tests/test_admin.py @@ -10,7 +10,9 @@ from django.test import Client, RequestFactory, TestCase, override_settings from allianceauth.authentication.models import CharacterOwnership, State from allianceauth.eveonline.models import ( - EveAllianceInfo, EveCharacter, EveCorporationInfo, + EveAllianceInfo, + EveCharacter, + EveCorporationInfo, ) from allianceauth.tests.auth_utils import AuthUtils diff --git a/allianceauth/groupmanagement/tests/test_managers.py b/allianceauth/groupmanagement/tests/test_managers.py index f656d99e..2e7ddcb9 100644 --- a/allianceauth/groupmanagement/tests/test_managers.py +++ b/allianceauth/groupmanagement/tests/test_managers.py @@ -1,14 +1,14 @@ from django.contrib.auth.models import Group, User from django.test import TestCase -from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo +from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from ..models import GroupRequest from ..managers import GroupManager +from ..models import GroupRequest -class MockUserNotAuthenticated(): +class MockUserNotAuthenticated: def __init__(self): self.is_authenticated = False diff --git a/allianceauth/groupmanagement/tests/test_signals.py b/allianceauth/groupmanagement/tests/test_signals.py index efa35dda..f398f288 100644 --- a/allianceauth/groupmanagement/tests/test_signals.py +++ b/allianceauth/groupmanagement/tests/test_signals.py @@ -1,11 +1,10 @@ +from django.contrib.auth.models import Group, User from django.test import TestCase -from django.contrib.auth.models import User, Group -from allianceauth.eveonline.models import EveCorporationInfo from allianceauth.eveonline.autogroups.models import AutogroupsConfig +from allianceauth.eveonline.models import EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils - from ..models import ReservedGroupName diff --git a/allianceauth/groupmanagement/urls.py b/allianceauth/groupmanagement/urls.py index 5b27821b..a2d78830 100644 --- a/allianceauth/groupmanagement/urls.py +++ b/allianceauth/groupmanagement/urls.py @@ -1,4 +1,5 @@ from django.urls import path + from . import views app_name = "groupmanagement" diff --git a/allianceauth/groupmanagement/views.py b/allianceauth/groupmanagement/views.py index 1d1bba87..1c736573 100644 --- a/allianceauth/groupmanagement/views.py +++ b/allianceauth/groupmanagement/views.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) @login_required @user_passes_test(GroupManager.can_manage_groups) def group_management(request): - logger.debug("group_management called by user %s" % request.user) + logger.debug(f"group_management called by user {request.user}") acceptrequests = [] leaverequests = [] @@ -40,8 +40,7 @@ def group_management(request): else: acceptrequests.append(grouprequest) - logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format( - request.user, len(acceptrequests), len(leaverequests))) + logger.debug(f"Providing user {request.user} with {len(acceptrequests)} acceptrequests and {len(leaverequests)} leaverequests.") show_leave_tab = ( getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False) @@ -60,7 +59,7 @@ def group_management(request): @login_required @user_passes_test(GroupManager.can_manage_groups) def group_membership(request): - logger.debug("group_membership called by user %s" % request.user) + logger.debug(f"group_membership called by user {request.user}") # Get all open and closed groups if GroupManager.has_management_permission(request.user): # Full access @@ -79,7 +78,7 @@ def group_membership(request): @login_required @user_passes_test(GroupManager.can_manage_groups) def group_membership_audit(request, group_id): - logger.debug("group_management_audit called by user %s" % request.user) + logger.debug(f"group_management_audit called by user {request.user}") group = get_object_or_404(Group, id=group_id) try: # Check its a joinable group i.e. not corp or internal @@ -101,8 +100,8 @@ def group_membership_audit(request, group_id): @user_passes_test(GroupManager.can_manage_groups) def group_membership_list(request, group_id): logger.debug( - "group_membership_list called by user %s " - "for group id %s" % (request.user, group_id) + f"group_membership_list called by user {request.user} " + f"for group id {group_id}" ) group = get_object_or_404(Group, id=group_id) try: @@ -113,8 +112,8 @@ def group_membership_list(request, group_id): or not GroupManager.can_manage_group(request.user, group) ): logger.warning( - "User %s attempted to view the membership of group %s " - "but permission was denied" % (request.user, group_id) + f"User {request.user} attempted to view the membership of group {group_id} " + "but permission was denied" ) raise PermissionDenied @@ -122,7 +121,7 @@ def group_membership_list(request, group_id): raise Http404("Group does not exist") group_leaders = group.authgroup.group_leaders.all() - members = list() + members = [] for member in \ group.user_set\ .all()\ @@ -190,20 +189,18 @@ def group_accept_request(request, group_request_id): log = RequestLog(request_type=group_request.leave_request,group=group,request_info=group_request.__str__(),action=1,request_actor=request.user) log.save() group_request.delete() - logger.info("User {} accepted group request from user {} to group {}".format( - request.user, group_request.user, group_request.group.name)) + logger.info(f"User {request.user} accepted group request from user {group_request.user} to group {group_request.group.name}") notify(group_request.user, "Group Application Accepted", level="success", - message="Your application to %s has been accepted." % group_request.group) + message=f"Your application to {group_request.group} has been accepted.") messages.success(request, _('Accepted application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) except PermissionDenied as p: logger.warning(f"User {request.user} attempted to accept group join request {group_request_id} but permission was denied") raise p - except: + except Exception: messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) - logger.exception("Unhandled exception occurred while user {} attempting to accept grouprequest id {}.".format( - request.user, group_request_id)) + logger.exception(f"Unhandled exception occurred while user {request.user} attempting to accept grouprequest id {group_request_id}.") pass return redirect("groupmanagement:management") @@ -219,22 +216,20 @@ def group_reject_request(request, group_request_id): raise PermissionDenied if group_request: - logger.info("User {} rejected group request from user {} to group {}".format( - request.user, group_request.user, group_request.group.name)) + logger.info(f"User {request.user} rejected group request from user {group_request.user} to group {group_request.group.name}") log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user) log.save() group_request.delete() - notify(group_request.user, "Group Application Rejected", level="danger", message="Your application to %s has been rejected." % group_request.group) + notify(group_request.user, "Group Application Rejected", level="danger", message=f"Your application to {group_request.group} has been rejected.") messages.success(request, _('Rejected application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) except PermissionDenied as p: logger.warning(f"User {request.user} attempted to reject group join request {group_request_id} but permission was denied") raise p - except: + except Exception: messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) - logger.exception("Unhandled exception occurred while user {} attempting to reject group request id {}".format( - request.user, group_request_id)) + logger.exception(f"Unhandled exception occurred while user {request.user} attempting to reject group request id {group_request_id}") pass return redirect("groupmanagement:management") @@ -256,20 +251,18 @@ def group_leave_accept_request(request, group_request_id): log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=1,request_actor=request.user) log.save() group_request.delete() - logger.info("User {} accepted group leave request from user {} to group {}".format( - request.user, group_request.user, group_request.group.name)) + logger.info(f"User {request.user} accepted group leave request from user {group_request.user} to group {group_request.group.name}") notify(group_request.user, "Group Leave Request Accepted", level="success", - message="Your request to leave %s has been accepted." % group_request.group) + message=f"Your request to leave {group_request.group} has been accepted.") messages.success(request, _('Accepted application from %(mainchar)s to leave %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) except PermissionDenied as p: logger.warning(f"User {request.user} attempted to accept group leave request {group_request_id} but permission was denied") raise p - except: + except Exception: messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % { "mainchar": group_request.main_char, "group": group_request.group}) - logger.exception("Unhandled exception occurred while user {} attempting to accept group leave request id {}".format( - request.user, group_request_id)) + logger.exception(f"Unhandled exception occurred while user {request.user} attempting to accept group leave request id {group_request_id}") pass return redirect("groupmanagement:management") @@ -289,19 +282,17 @@ def group_leave_reject_request(request, group_request_id): log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user) log.save() group_request.delete() - logger.info("User {} rejected group leave request from user {} for group {}".format( - request.user, group_request.user, group_request.group.name)) - notify(group_request.user, "Group Leave Request Rejected", level="danger", message="Your request to leave %s has been rejected." % group_request.group) + logger.info(f"User {request.user} rejected group leave request from user {group_request.user} for group {group_request.group.name}") + notify(group_request.user, "Group Leave Request Rejected", level="danger", message=f"Your request to leave {group_request.group} has been rejected.") messages.success(request, _('Rejected application from %(mainchar)s to leave %(group)s.') % { "mainchar": group_request.main_char, "group": group_request.group}) except PermissionDenied as p: logger.warning(f"User {request.user} attempted to reject group leave request {group_request_id} but permission was denied") raise p - except: + except Exception: messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % { "mainchar": group_request.main_char, "group": group_request.group}) - logger.exception("Unhandled exception occurred while user {} attempting to reject group leave request id {}".format( - request.user, group_request_id)) + logger.exception(f"Unhandled exception occurred while user {request.user} attempting to reject group leave request id {group_request_id}") pass return redirect("groupmanagement:management") @@ -309,7 +300,7 @@ def group_leave_reject_request(request, group_request_id): @login_required def groups_view(request): - logger.debug("groups_view called by user %s" % request.user) + logger.debug(f"groups_view called by user {request.user}") groups_qs = GroupManager.get_joinable_groups_for_user( request.user, include_hidden=False diff --git a/allianceauth/hooks.py b/allianceauth/hooks.py index 0a91942b..7262f53e 100644 --- a/allianceauth/hooks.py +++ b/allianceauth/hooks.py @@ -30,13 +30,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Based on https://github.com/torchbox/wagtail/blob/master/wagtail/wagtailcore/hooks.py """ +import logging from importlib import import_module from django.apps import apps from django.utils.module_loading import module_has_submodule -import logging - logger = logging.getLogger(__name__) _hooks = {} # Dict of Name: Fn's of registered hooks @@ -64,7 +63,7 @@ def register(name, fn=None): """ def _hook_add(func): if name not in _hooks: - logger.debug("Creating new hook %s" % name) + logger.debug(f"Creating new hook {name}") _hooks[name] = [] logger.debug(f'Registering hook {name} for function {fn}') @@ -110,7 +109,7 @@ def register_all_hooks(): if not _all_hooks_registered: logger.debug("Searching for hooks") hooks = list(get_app_submodules('auth_hooks')) - logger.debug("Got %s hooks" % len(hooks)) + logger.debug(f"Got {len(hooks)} hooks") _all_hooks_registered = True @@ -133,6 +132,6 @@ class DashboardItemHook: try: logger.debug(f"Rendering {self.view_function} to dashboard") return self.view_function(request) - except Exception as e: + except Exception: logger.exception(f"Rendering {self.view_function} failed!") return "" diff --git a/allianceauth/hrapplications/admin.py b/allianceauth/hrapplications/admin.py index f1075da7..3381da0f 100644 --- a/allianceauth/hrapplications/admin.py +++ b/allianceauth/hrapplications/admin.py @@ -1,7 +1,13 @@ from django.contrib import admin -from .models import Application, ApplicationChoice, ApplicationComment, ApplicationForm, ApplicationQuestion, \ - ApplicationResponse +from .models import ( + Application, + ApplicationChoice, + ApplicationComment, + ApplicationForm, + ApplicationQuestion, + ApplicationResponse, +) class ChoiceInline(admin.TabularInline): diff --git a/allianceauth/hrapplications/managers.py b/allianceauth/hrapplications/managers.py index 7c518520..4e41e5c9 100644 --- a/allianceauth/hrapplications/managers.py +++ b/allianceauth/hrapplications/managers.py @@ -1,6 +1,6 @@ + from django.contrib.auth.models import User from django.db import models -from typing import Optional class ApplicationManager(models.Manager): diff --git a/allianceauth/hrapplications/migrations/0001_initial.py b/allianceauth/hrapplications/migrations/0001_initial.py index 466e8b18..7b094c65 100644 --- a/allianceauth/hrapplications/migrations/0001_initial.py +++ b/allianceauth/hrapplications/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:39 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/hrapplications/migrations/0002_choices_for_questions.py b/allianceauth/hrapplications/migrations/0002_choices_for_questions.py index 4687a7d6..01815ff7 100644 --- a/allianceauth/hrapplications/migrations/0002_choices_for_questions.py +++ b/allianceauth/hrapplications/migrations/0002_choices_for_questions.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.4 on 2017-08-23 19:46 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/hrapplications/migrations/0005_sorted_questions.py b/allianceauth/hrapplications/migrations/0005_sorted_questions.py index 7e32e458..5eabae54 100644 --- a/allianceauth/hrapplications/migrations/0005_sorted_questions.py +++ b/allianceauth/hrapplications/migrations/0005_sorted_questions.py @@ -1,9 +1,10 @@ # Generated by Django 1.10.5 on 2017-03-27 03:29 -from django.db import migrations import sortedm2m.fields from sortedm2m.operations import AlterSortedManyToManyField +from django.db import migrations + class Migration(migrations.Migration): diff --git a/allianceauth/hrapplications/models.py b/allianceauth/hrapplications/models.py index 4767ec7f..2b3414d6 100644 --- a/allianceauth/hrapplications/models.py +++ b/allianceauth/hrapplications/models.py @@ -1,6 +1,7 @@ +from sortedm2m.fields import SortedManyToManyField + from django.contrib.auth.models import User from django.db import models -from sortedm2m.fields import SortedManyToManyField from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo @@ -9,7 +10,7 @@ from .managers import ApplicationManager class ApplicationQuestion(models.Model): title = models.CharField(max_length=254, verbose_name='Question') - help_text = models.CharField(max_length=254, blank=True, null=True) + help_text = models.CharField(max_length=254, blank=True) multi_select = models.BooleanField(default=False) def __str__(self): @@ -42,8 +43,6 @@ class Application(models.Model): objects = ApplicationManager() - def __str__(self): - return str(self.user) + " Application To " + str(self.form) class Meta: permissions = ( @@ -51,6 +50,9 @@ class Application(models.Model): ('view_apis', 'Can view applicant APIs'),) unique_together = ('form', 'user') + def __str__(self): + return str(self.user) + " Application To " + str(self.form) + @property def main_character(self): return self.user.profile.main_character @@ -73,12 +75,12 @@ class ApplicationResponse(models.Model): question = models.ForeignKey(ApplicationQuestion, on_delete=models.CASCADE) application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='responses') answer = models.TextField() - + class Meta: + unique_together = ('question', 'application') def __str__(self): return str(self.application) + " Answer To " + str(self.question) - class Meta: - unique_together = ('question', 'application') + class ApplicationComment(models.Model): diff --git a/allianceauth/hrapplications/tests.py b/allianceauth/hrapplications/tests.py index 7dab4cd1..b9b8ff1e 100644 --- a/allianceauth/hrapplications/tests.py +++ b/allianceauth/hrapplications/tests.py @@ -4,7 +4,7 @@ from django.test import TestCase from allianceauth.eveonline.models import EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from .models import Application, ApplicationForm, ApplicationQuestion, ApplicationChoice +from .models import Application, ApplicationChoice, ApplicationForm, ApplicationQuestion class TestApplicationManagersPendingRequestsCountForUser(TestCase): diff --git a/allianceauth/hrapplications/views.py b/allianceauth/hrapplications/views.py index 789428e2..2102b663 100644 --- a/allianceauth/hrapplications/views.py +++ b/allianceauth/hrapplications/views.py @@ -1,18 +1,13 @@ import logging -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required -from django.contrib.auth.decorators import user_passes_test -from django.shortcuts import render, get_object_or_404, redirect, Http404 +from django.contrib.auth.decorators import login_required, permission_required, user_passes_test from django.db.models import Q -from .models import Application -from .models import ApplicationComment -from .models import ApplicationForm -from .models import ApplicationResponse +from django.shortcuts import Http404, get_object_or_404, redirect, render + from allianceauth.notifications import notify -from .forms import HRApplicationCommentForm -from .forms import HRApplicationSearchForm +from .forms import HRApplicationCommentForm, HRApplicationSearchForm +from .models import Application, ApplicationComment, ApplicationForm, ApplicationResponse logger = logging.getLogger(__name__) @@ -23,7 +18,7 @@ def create_application_test(user): @login_required def hr_application_management_view(request): - logger.debug("hr_application_management_view called by user %s" % request.user) + logger.debug(f"hr_application_management_view called by user {request.user}") corp_applications = [] finished_corp_applications = [] main_char = request.user.profile.main_character @@ -38,8 +33,7 @@ def hr_application_management_view(request): corp_applications = base_app_query.filter(form=app_form).filter(approved=None).order_by('-created') finished_corp_applications = base_app_query.filter(form=app_form).filter( approved__in=[True, False]).order_by('-created') - logger.debug("Retrieved {} personal, {} corp applications for {}".format( - len(request.user.applications.all()), len(corp_applications), request.user)) + logger.debug(f"Retrieved {len(request.user.applications.all())} personal, {len(corp_applications)} corp applications for {request.user}") context = { 'personal_apps': request.user.applications.all(), 'applications': corp_applications, @@ -122,7 +116,7 @@ def hr_application_view(request, app_id): if request.method == 'POST': if request.user.has_perm('hrapplications.add_applicationcomment'): form = HRApplicationCommentForm(request.POST) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): comment = ApplicationComment() comment.application = app @@ -132,7 +126,7 @@ def hr_application_view(request, app_id): logger.info(f"Saved comment by user {request.user} to {app}") return redirect('hrapplications:view', app_id) else: - logger.warning("User %s does not have permission to add ApplicationComments" % request.user) + logger.warning(f"User {request.user} does not have permission to add ApplicationComments") return redirect('hrapplications:view', app_id) else: logger.debug("Returning blank HRApplication comment form.") @@ -155,7 +149,7 @@ def hr_application_remove(request, app_id): app = get_object_or_404(Application, pk=app_id) logger.info(f"User {request.user} deleting {app}") app.delete() - notify(app.user, "Application Deleted", message="Your application to %s was deleted." % app.form.corp) + notify(app.user, "Application Deleted", message=f"Your application to {app.form.corp} was deleted.") return redirect('hrapplications:index') @@ -169,7 +163,7 @@ def hr_application_approve(request, app_id): logger.info(f"User {request.user} approving {app}") app.approved = True app.save() - notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success") + notify(app.user, "Application Accepted", message=f"Your application to {app.form.corp} has been approved.", level="success") else: logger.warning(f"User {request.user} not authorized to approve {app}") return redirect('hrapplications:index') @@ -185,7 +179,7 @@ def hr_application_reject(request, app_id): logger.info(f"User {request.user} rejecting {app}") app.approved = False app.save() - notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger") + notify(app.user, "Application Rejected", message=f"Your application to {app.form.corp} has been rejected.", level="danger") else: logger.warning(f"User {request.user} not authorized to reject {app}") return redirect('hrapplications:index') @@ -194,10 +188,10 @@ def hr_application_reject(request, app_id): @login_required @permission_required('auth.human_resources') def hr_application_search(request): - logger.debug("hr_application_search called by user %s" % request.user) + logger.debug(f"hr_application_search called by user {request.user}") if request.method == 'POST': form = HRApplicationSearchForm(request.POST) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): searchstring = form.cleaned_data['search_string'].lower() applications = set() @@ -209,7 +203,7 @@ def hr_application_search(request): form__corp__corporation_id=request.user.profile.main_character.corporation_id) except AttributeError: logger.warning( - "User %s missing main character model: unable to filter applications to search" % request.user) + f"User {request.user} missing main character model: unable to filter applications to search") applications = app_list.filter( Q(user__profile__main_character__character_name__icontains=searchstring) | @@ -225,12 +219,12 @@ def hr_application_search(request): return render(request, 'hrapplications/searchview.html', context=context) else: - logger.debug("Form invalid - returning for user %s to retry." % request.user) + logger.debug(f"Form invalid - returning for user {request.user} to retry.") context = {'applications': None, 'search_form': form} return render(request, 'hrapplications/searchview.html', context=context) else: - logger.debug("Returning empty search form for user %s" % request.user) + logger.debug(f"Returning empty search form for user {request.user}") return redirect("hrapplications:view") diff --git a/allianceauth/menu/admin.py b/allianceauth/menu/admin.py index 7df1c250..381fb206 100644 --- a/allianceauth/menu/admin.py +++ b/allianceauth/menu/admin.py @@ -1,6 +1,5 @@ """Admin site for menu app.""" -from typing import Optional from django.contrib import admin from django.http import HttpRequest, HttpResponse diff --git a/allianceauth/menu/core/menu_item_hooks.py b/allianceauth/menu/core/menu_item_hooks.py index 2114569e..c0d26e5b 100644 --- a/allianceauth/menu/core/menu_item_hooks.py +++ b/allianceauth/menu/core/menu_item_hooks.py @@ -1,7 +1,7 @@ """Logic for handling MenuItemHook objects.""" import hashlib -from typing import List, NamedTuple, Optional +from typing import NamedTuple from allianceauth.menu.hooks import MenuItemHook diff --git a/allianceauth/menu/hooks.py b/allianceauth/menu/hooks.py index 9341415c..8a1fe5d9 100644 --- a/allianceauth/menu/hooks.py +++ b/allianceauth/menu/hooks.py @@ -1,6 +1,5 @@ """Menu item hooks.""" -from typing import List, Optional from django.template.loader import render_to_string diff --git a/allianceauth/menu/migrations/0001_initial.py b/allianceauth/menu/migrations/0001_initial.py index 1594566b..bf25eebb 100644 --- a/allianceauth/menu/migrations/0001_initial.py +++ b/allianceauth/menu/migrations/0001_initial.py @@ -1,7 +1,7 @@ # Generated by Django 4.2.9 on 2024-02-15 00:01 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/menu/models.py b/allianceauth/menu/models.py index b2984842..40c91aab 100644 --- a/allianceauth/menu/models.py +++ b/allianceauth/menu/models.py @@ -48,8 +48,13 @@ class MenuItem(models.Model): # app related properties hook_hash = models.CharField( - max_length=64, default=None, null=True, unique=True, editable=False - ) # hash of a menu item hook. Must be nullable for unique comparison. + max_length=64, + default=None, + null=True, + unique=True, + editable=False, + help_text="hash of a menu item hook. Must be nullable for unique comparison." + ) # user defined properties classes = models.CharField( diff --git a/allianceauth/menu/templatetags/menu_menu_items.py b/allianceauth/menu/templatetags/menu_menu_items.py index 2049e56d..6f93b9fb 100644 --- a/allianceauth/menu/templatetags/menu_menu_items.py +++ b/allianceauth/menu/templatetags/menu_menu_items.py @@ -24,7 +24,6 @@ which is used to render the complete menu. """ from dataclasses import dataclass, field -from typing import Dict, List, Optional from django import template from django.db.models import QuerySet diff --git a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py index ff48a49a..beae2da4 100644 --- a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py +++ b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py @@ -1,4 +1,4 @@ -from typing import List, NamedTuple, Optional +from typing import NamedTuple from unittest.mock import patch from bs4 import BeautifulSoup diff --git a/allianceauth/notifications/admin.py b/allianceauth/notifications/admin.py index 3fb3242a..b2afd02b 100644 --- a/allianceauth/notifications/admin.py +++ b/allianceauth/notifications/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Notification diff --git a/allianceauth/notifications/handlers.py b/allianceauth/notifications/handlers.py index b42aa09f..34d15da5 100644 --- a/allianceauth/notifications/handlers.py +++ b/allianceauth/notifications/handlers.py @@ -5,8 +5,9 @@ logger = logging.getLogger(__name__) class NotificationHandler(logging.Handler): def emit(self, record): - from django.contrib.auth.models import User, Permission + from django.contrib.auth.models import Permission, User from django.db.models import Q + from . import notify from .models import Notification diff --git a/allianceauth/notifications/managers.py b/allianceauth/notifications/managers.py index 3798428e..42bbc01e 100644 --- a/allianceauth/notifications/managers.py +++ b/allianceauth/notifications/managers.py @@ -1,9 +1,9 @@ import logging from django.conf import settings +from django.contrib.auth.models import User from django.core.cache import cache from django.db import models -from django.contrib.auth.models import User logger = logging.getLogger(__name__) diff --git a/allianceauth/notifications/migrations/0001_initial.py b/allianceauth/notifications/migrations/0001_initial.py index 867ca680..21cdcf0e 100644 --- a/allianceauth/notifications/migrations/0001_initial.py +++ b/allianceauth/notifications/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:40 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/notifications/models.py b/allianceauth/notifications/models.py index dd77d0ff..6d75ff43 100644 --- a/allianceauth/notifications/models.py +++ b/allianceauth/notifications/models.py @@ -1,7 +1,7 @@ import logging -from django.db import models from django.contrib.auth.models import User +from django.db import models from django.utils.translation import gettext_lazy as _ from .managers import NotificationManager @@ -73,7 +73,7 @@ class Notification(models.Model): def mark_viewed(self) -> None: """Mark notification as viewed.""" - logger.info("Marking notification as viewed: %s" % self) + logger.info(f"Marking notification as viewed: {self}") self.viewed = True self.save() diff --git a/allianceauth/notifications/templatetags/auth_notifications.py b/allianceauth/notifications/templatetags/auth_notifications.py index 6ebce070..dcdf35ea 100644 --- a/allianceauth/notifications/templatetags/auth_notifications.py +++ b/allianceauth/notifications/templatetags/auth_notifications.py @@ -12,7 +12,6 @@ from django.contrib.auth.models import User from allianceauth.notifications.models import Notification - logger = logging.getLogger(__name__) register = template.Library() diff --git a/allianceauth/notifications/tests/test_handlers.py b/allianceauth/notifications/tests/test_handlers.py index f90a4726..aa3c0bc3 100644 --- a/allianceauth/notifications/tests/test_handlers.py +++ b/allianceauth/notifications/tests/test_handlers.py @@ -1,7 +1,8 @@ -from logging import LogRecord, DEBUG +from logging import DEBUG, LogRecord -from django.contrib.auth.models import Permission, Group, User +from django.contrib.auth.models import Group, Permission, User from django.test import TestCase + from allianceauth.tests.auth_utils import AuthUtils from ..handlers import NotificationHandler diff --git a/allianceauth/notifications/tests/test_init.py b/allianceauth/notifications/tests/test_init.py index e09e1ee7..3cab1ed7 100644 --- a/allianceauth/notifications/tests/test_init.py +++ b/allianceauth/notifications/tests/test_init.py @@ -1,4 +1,5 @@ from django.test import TestCase + from allianceauth.tests.auth_utils import AuthUtils from .. import notify diff --git a/allianceauth/notifications/tests/test_managers.py b/allianceauth/notifications/tests/test_managers.py index a74ca2b9..e21c6aa2 100644 --- a/allianceauth/notifications/tests/test_managers.py +++ b/allianceauth/notifications/tests/test_managers.py @@ -5,8 +5,8 @@ from django.contrib.auth.models import User from django.test import TestCase, override_settings from allianceauth.tests.auth_utils import AuthUtils -from ..models import Notification +from ..models import Notification MODULE_PATH = 'allianceauth.notifications.models' diff --git a/allianceauth/notifications/tests/test_models.py b/allianceauth/notifications/tests/test_models.py index b291f2c8..320a3c45 100644 --- a/allianceauth/notifications/tests/test_models.py +++ b/allianceauth/notifications/tests/test_models.py @@ -1,11 +1,11 @@ from unittest.mock import patch from django.test import TestCase + from allianceauth.tests.auth_utils import AuthUtils from ..models import Notification - MODULE_PATH = 'allianceauth.notifications.models' diff --git a/allianceauth/notifications/tests/test_templatetags.py b/allianceauth/notifications/tests/test_templatetags.py index 587970be..f31a9a45 100644 --- a/allianceauth/notifications/tests/test_templatetags.py +++ b/allianceauth/notifications/tests/test_templatetags.py @@ -1,11 +1,10 @@ -from unittest.mock import patch, Mock +from unittest.mock import patch from django.test import TestCase, override_settings from allianceauth.tests.auth_utils import AuthUtils -from ..templatetags.auth_notifications import ( - user_unread_notification_count, notifications_refresh_time -) + +from ..templatetags.auth_notifications import notifications_refresh_time, user_unread_notification_count MODULE_PATH = 'allianceauth.notifications.templatetags.auth_notifications' diff --git a/allianceauth/notifications/tests/test_views.py b/allianceauth/notifications/tests/test_views.py index 7fa95bc8..ca6212f4 100644 --- a/allianceauth/notifications/tests/test_views.py +++ b/allianceauth/notifications/tests/test_views.py @@ -1,15 +1,13 @@ import json +from unittest.mock import patch -from unittest.mock import patch, Mock - -from django.test import TestCase, RequestFactory +from django.test import RequestFactory, TestCase from django.urls import reverse from allianceauth.tests.auth_utils import AuthUtils from ..views import user_notifications_count - MODULE_PATH = 'allianceauth.notifications.views' diff --git a/allianceauth/notifications/urls.py b/allianceauth/notifications/urls.py index 694a700b..be8f7073 100644 --- a/allianceauth/notifications/urls.py +++ b/allianceauth/notifications/urls.py @@ -1,4 +1,5 @@ from django.urls import path + from . import views app_name = 'notifications' diff --git a/allianceauth/notifications/views.py b/allianceauth/notifications/views.py index 40bc88f5..3d3a586f 100644 --- a/allianceauth/notifications/views.py +++ b/allianceauth/notifications/views.py @@ -1,9 +1,9 @@ import logging -from django.contrib.auth.decorators import login_required from django.contrib import messages +from django.contrib.auth.decorators import login_required from django.http import JsonResponse -from django.shortcuts import render, get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ from .models import Notification @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) @login_required def notification_list(request): - logger.debug("notification_list called by user %s" % request.user) + logger.debug(f"notification_list called by user {request.user}") notifications_qs = Notification.objects.filter(user=request.user).order_by("-timestamp") new_notifs = notifications_qs.filter(viewed=False) old_notifs = notifications_qs.filter(viewed=True) diff --git a/allianceauth/optimer/auth_hooks.py b/allianceauth/optimer/auth_hooks.py index b14a41b5..e0fefb16 100644 --- a/allianceauth/optimer/auth_hooks.py +++ b/allianceauth/optimer/auth_hooks.py @@ -1,8 +1,10 @@ +from django.utils.translation import gettext_lazy as _ + +from allianceauth import hooks from allianceauth.menu.hooks import MenuItemHook from allianceauth.optimer.views import dashboard_ops from allianceauth.services.hooks import UrlHook -from django.utils.translation import gettext_lazy as _ -from allianceauth import hooks + from . import urls diff --git a/allianceauth/optimer/form_widgets.py b/allianceauth/optimer/form_widgets.py index e639e18b..b52dad0e 100644 --- a/allianceauth/optimer/form_widgets.py +++ b/allianceauth/optimer/form_widgets.py @@ -18,7 +18,7 @@ class DataListWidget(forms.TextInput): self._name = name self._list = data_list - self.attrs.update({"list": "list__%s" % self._name}) + self.attrs.update({"list": f"list__{self._name}"}) def render(self, name, value, attrs=None, renderer=None): """ @@ -36,10 +36,10 @@ class DataListWidget(forms.TextInput): """ text_html = super().render(name, value, attrs=attrs) - data_list = '' % self._name + data_list = f'' for item in self._list: - data_list += '" diff --git a/allianceauth/optimer/migrations/0001_initial.py b/allianceauth/optimer/migrations/0001_initial.py index 7e57fa8a..29aeeeb6 100644 --- a/allianceauth/optimer/migrations/0001_initial.py +++ b/allianceauth/optimer/migrations/0001_initial.py @@ -1,9 +1,10 @@ # Generated by Django 1.10.1 on 2016-09-05 21:40 import datetime -from django.db import migrations, models + import django.db.models.deletion import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/optimer/migrations/0004_on_delete.py b/allianceauth/optimer/migrations/0004_on_delete.py index f07883c3..26565539 100644 --- a/allianceauth/optimer/migrations/0004_on_delete.py +++ b/allianceauth/optimer/migrations/0004_on_delete.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.5 on 2017-09-28 02:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/optimer/migrations/0005_add_type_and_description.py b/allianceauth/optimer/migrations/0005_add_type_and_description.py index c8fee43d..533417e1 100644 --- a/allianceauth/optimer/migrations/0005_add_type_and_description.py +++ b/allianceauth/optimer/migrations/0005_add_type_and_description.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.8 on 2021-10-26 16:20 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/optimer/models.py b/allianceauth/optimer/models.py index 02139a93..daa7a80e 100644 --- a/allianceauth/optimer/models.py +++ b/allianceauth/optimer/models.py @@ -13,18 +13,17 @@ class OpTimerType(models.Model): type = models.CharField(max_length=254, default="") - def __str__(self): - return self.type + class Meta: ordering = ['type'] default_permissions = () + def __str__(self): + return self.type class OpTimer(models.Model): - class Meta: - ordering = ['start'] - default_permissions = () + doctrine = models.CharField(max_length=254, default="") system = models.CharField(max_length=254, default="") @@ -37,6 +36,8 @@ class OpTimer(models.Model): on_delete=models.SET_NULL) description = models.TextField(blank=True, default="") type = models.ForeignKey(OpTimerType, null=True, on_delete=models.SET_NULL) - + class Meta: + ordering = ['start'] + default_permissions = () def __str__(self): return self.operation_name diff --git a/allianceauth/optimer/views.py b/allianceauth/optimer/views.py index 760879e6..1b815e7b 100644 --- a/allianceauth/optimer/views.py +++ b/allianceauth/optimer/views.py @@ -1,10 +1,8 @@ import logging from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required -from django.shortcuts import get_object_or_404 -from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -29,7 +27,7 @@ def optimer_view(request): :rtype: """ - logger.debug("optimer_view called by user %s" % request.user) + logger.debug(f"optimer_view called by user {request.user}") base_query = OpTimer.objects.select_related('eve_character', 'type') render_items = {'optimer': base_query.all(), 'future_timers': base_query.filter( @@ -52,11 +50,11 @@ def add_optimer_view(request): :rtype: """ - logger.debug("add_optimer_view called by user %s" % request.user) + logger.debug(f"add_optimer_view called by user {request.user}") if request.method == 'POST': form = OpForm(request.POST, data_list=OpTimerType.objects.all()) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): optimer_type = None @@ -141,7 +139,7 @@ def edit_optimer(request, optimer_id): if request.method == 'POST': form = OpForm(request.POST, data_list=OpTimerType.objects.all()) - logger.debug("Received POST request containing update optimer form, is valid: %s" % form.is_valid()) + logger.debug(f"Received POST request containing update optimer form, is valid: {form.is_valid()}") if form.is_valid(): character = request.user.profile.main_character diff --git a/allianceauth/permissions_tool/auth_hooks.py b/allianceauth/permissions_tool/auth_hooks.py index f89444d5..4171f14c 100644 --- a/allianceauth/permissions_tool/auth_hooks.py +++ b/allianceauth/permissions_tool/auth_hooks.py @@ -1,9 +1,9 @@ -from allianceauth.menu.hooks import MenuItemHook -from . import urls - from allianceauth import hooks +from allianceauth.menu.hooks import MenuItemHook from allianceauth.services.hooks import UrlHook +from . import urls + class PermissionsTool(MenuItemHook): def __init__(self): diff --git a/allianceauth/permissions_tool/models.py b/allianceauth/permissions_tool/models.py index 3a93f94b..bce74dc9 100644 --- a/allianceauth/permissions_tool/models.py +++ b/allianceauth/permissions_tool/models.py @@ -10,3 +10,6 @@ class PermissionsTool(models.Model): permissions = ( ('audit_permissions', 'Can audit permissions'), ) + + def __str__(self) -> str: + return f"{self.pk}" diff --git a/allianceauth/permissions_tool/tests.py b/allianceauth/permissions_tool/tests.py index 9354a0e3..0fe8e4b5 100644 --- a/allianceauth/permissions_tool/tests.py +++ b/allianceauth/permissions_tool/tests.py @@ -1,9 +1,11 @@ from django_webtest import WebTest + from django import urls from django.contrib.auth.models import Group, Permission from allianceauth.tests.auth_utils import AuthUtils + class PermissionsToolViewsTestCase(WebTest): def setUp(self): self.member = AuthUtils.create_member('auth_member') diff --git a/allianceauth/permissions_tool/urls.py b/allianceauth/permissions_tool/urls.py index 7a66c19a..0cced253 100644 --- a/allianceauth/permissions_tool/urls.py +++ b/allianceauth/permissions_tool/urls.py @@ -1,5 +1,4 @@ -from django.urls import re_path -from django.urls import path +from django.urls import path, re_path from . import views diff --git a/allianceauth/permissions_tool/views.py b/allianceauth/permissions_tool/views.py index e845a466..de4cc281 100644 --- a/allianceauth/permissions_tool/views.py +++ b/allianceauth/permissions_tool/views.py @@ -1,10 +1,9 @@ import logging from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import Permission, User +from django.contrib.auth.models import Permission from django.db.models import Count -from django.shortcuts import render, Http404 - +from django.shortcuts import Http404, render logger = logging.getLogger(__name__) @@ -12,7 +11,7 @@ logger = logging.getLogger(__name__) @login_required @permission_required('permissions_tool.audit_permissions') def permissions_overview(request): - logger.debug("permissions_overview called by user %s" % request.user) + logger.debug(f"permissions_overview called by user {request.user}") perms = Permission.objects.select_related('content_type').all()\ .annotate(Count('user', distinct=True))\ .annotate(Count('group', distinct=True)) \ diff --git a/allianceauth/project_template/manage.py b/allianceauth/project_template/manage.py index 843d8a7d..acf38748 100644 --- a/allianceauth/project_template/manage.py +++ b/allianceauth/project_template/manage.py @@ -6,17 +6,17 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name}}.settings.local") try: from django.core.management import execute_from_command_line - except ImportError: + except ImportError as err: # The above import may fail for some other reason. Ensure that the # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django + import django # noqa: F401 except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) + ) from err raise execute_from_command_line(sys.argv) diff --git a/allianceauth/project_template/project_name/__init__.py b/allianceauth/project_template/project_name/__init__.py index cf2e85f6..dfa885c1 100644 --- a/allianceauth/project_template/project_name/__init__.py +++ b/allianceauth/project_template/project_name/__init__.py @@ -1 +1 @@ -from .celery import app as celery_app +from .celery import app as celery_app # noqa:F401 diff --git a/allianceauth/project_template/project_name/celery.py b/allianceauth/project_template/project_name/celery.py index 35ec18dd..bfa29c89 100644 --- a/allianceauth/project_template/project_name/celery.py +++ b/allianceauth/project_template/project_name/celery.py @@ -1,11 +1,12 @@ import os + from celery import Celery from celery.app import trace # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings.local') -from django.conf import settings # noqa +from django.conf import settings app = Celery('{{ project_name }}') diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py index fb18e4de..bed938ae 100644 --- a/allianceauth/project_template/project_name/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -11,7 +11,6 @@ import os from celery.schedules import crontab from django.contrib import messages - from django.utils.translation import gettext_lazy as _ INSTALLED_APPS = [ diff --git a/allianceauth/project_template/project_name/urls.py b/allianceauth/project_template/project_name/urls.py index 58d40642..a098e831 100644 --- a/allianceauth/project_template/project_name/urls.py +++ b/allianceauth/project_template/project_name/urls.py @@ -1,6 +1,7 @@ -from allianceauth import urls from django.urls import include, path +from allianceauth import urls + urlpatterns = [ path('', include(urls)), ] diff --git a/allianceauth/services/abstract.py b/allianceauth/services/abstract.py index d6e500e0..034d1dfc 100644 --- a/allianceauth/services/abstract.py +++ b/allianceauth/services/abstract.py @@ -7,22 +7,22 @@ to use these views. You are free to build the internal structure of the service as you like. """ +import logging from collections import OrderedDict -from django.views import View -from django.urls import reverse_lazy -from django.views.generic import UpdateView, DeleteView -from django.views.generic.detail import SingleObjectMixin + from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.db import models, IntegrityError from django.core.exceptions import ObjectDoesNotExist -from django.shortcuts import render, Http404, redirect +from django.db import IntegrityError, models +from django.shortcuts import Http404, redirect, render +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.generic import DeleteView, UpdateView +from django.views.generic.detail import SingleObjectMixin from .forms import ServicePasswordModelForm -import logging - logger = logging.getLogger(__name__) @@ -33,6 +33,10 @@ class AbstractServiceModel(models.Model): related_name='%(app_label)s' ) + + class Meta: + abstract = True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.credentials = OrderedDict() @@ -44,10 +48,6 @@ class AbstractServiceModel(models.Model): def reset_password(self): pass - class Meta: - abstract = True - - class BaseServiceView(LoginRequiredMixin, PermissionRequiredMixin, View): """ Define: diff --git a/allianceauth/services/apps.py b/allianceauth/services/apps.py index 5160ce48..4412f394 100644 --- a/allianceauth/services/apps.py +++ b/allianceauth/services/apps.py @@ -6,4 +6,4 @@ class ServicesConfig(AppConfig): label = 'services' def ready(self): - from . import signals + pass diff --git a/allianceauth/services/auth_hooks.py b/allianceauth/services/auth_hooks.py index cda56f58..030860b5 100644 --- a/allianceauth/services/auth_hooks.py +++ b/allianceauth/services/auth_hooks.py @@ -1,6 +1,7 @@ -from allianceauth import hooks from django.utils.translation import gettext_lazy as _ +from allianceauth import hooks + from ..menu.hooks import MenuItemHook from .hooks import ServicesHook diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 458f9266..108932f9 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -1,20 +1,13 @@ -from string import Formatter -from django.urls import include, re_path -from typing import Optional from collections.abc import Iterable +from string import Formatter from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.template.loader import render_to_string from django.urls import include, re_path from django.utils.functional import cached_property from allianceauth.hooks import get_hooks from allianceauth.menu.hooks import MenuItemHook -from django.conf import settings -from django.urls import include, re_path -from django.core.exceptions import ObjectDoesNotExist -from django.utils.functional import cached_property from .models import NameFormatConfig diff --git a/allianceauth/services/modules/discord/admin.py b/allianceauth/services/modules/discord/admin.py index 4db920bf..acc0c57d 100644 --- a/allianceauth/services/modules/discord/admin.py +++ b/allianceauth/services/modules/discord/admin.py @@ -4,9 +4,9 @@ from django.contrib import admin from ...admin import ServicesUserAdmin from . import __title__ +from .auth_hooks import DiscordService from .models import DiscordUser from .utils import LoggerAddTag -from .auth_hooks import DiscordService logger = LoggerAddTag(logging.getLogger(__name__), __title__) diff --git a/allianceauth/services/modules/discord/api.py b/allianceauth/services/modules/discord/api.py index 5805d730..c5cf6d55 100644 --- a/allianceauth/services/modules/discord/api.py +++ b/allianceauth/services/modules/discord/api.py @@ -18,12 +18,11 @@ Here is an example for using the api to fetch the current roles from the configu The docs for the client class can be found here: :py:class:`~allianceauth.services.modules.discord.discord_client.client.DiscordClient` """ -from typing import Optional from .app_settings import DISCORD_GUILD_ID -from .core import create_bot_client, group_to_role, server_name # noqa -from .discord_client.models import Role # noqa -from .models import DiscordUser # noqa +from .core import create_bot_client, group_to_role, server_name +from .discord_client.models import Role +from .models import DiscordUser __all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"] diff --git a/allianceauth/services/modules/discord/app_settings.py b/allianceauth/services/modules/discord/app_settings.py index 52f85f80..60a9072c 100644 --- a/allianceauth/services/modules/discord/app_settings.py +++ b/allianceauth/services/modules/discord/app_settings.py @@ -1,6 +1,5 @@ from .utils import clean_setting - DISCORD_APP_ID = clean_setting('DISCORD_APP_ID', '') """App ID for the AA bot on Discord. Needs to be set.""" diff --git a/allianceauth/services/modules/discord/auth_hooks.py b/allianceauth/services/modules/discord/auth_hooks.py index 37385a37..7ce3da2d 100644 --- a/allianceauth/services/modules/discord/auth_hooks.py +++ b/allianceauth/services/modules/discord/auth_hooks.py @@ -6,15 +6,12 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook +from . import __title__, tasks +from .app_settings import DISCORD_SYNC_NAMES from .core import server_name, user_formatted_nick from .models import DiscordUser from .urls import urlpatterns from .utils import LoggerAddTag -from . import tasks, __title__ -from .app_settings import ( - DISCORD_SYNC_NAMES -) - logger = LoggerAddTag(logging.getLogger(__name__), __title__) diff --git a/allianceauth/services/modules/discord/core.py b/allianceauth/services/modules/discord/core.py index 7b1f76e7..406b6291 100644 --- a/allianceauth/services/modules/discord/core.py +++ b/allianceauth/services/modules/discord/core.py @@ -1,7 +1,6 @@ """Core functionality of the Discord service not directly related to models.""" import logging -from typing import List, Optional, Tuple from requests.exceptions import HTTPError @@ -12,7 +11,7 @@ from allianceauth.services.hooks import NameFormatter from . import __title__ from .app_settings import DISCORD_BOT_TOKEN, DISCORD_GUILD_ID -from .discord_client import DiscordClient, RolesSet, Role +from .discord_client import DiscordClient, Role, RolesSet from .discord_client.exceptions import DiscordClientException from .utils import LoggerAddTag diff --git a/allianceauth/services/modules/discord/discord_client/app_settings.py b/allianceauth/services/modules/discord/discord_client/app_settings.py index c6ff7711..99dc078d 100644 --- a/allianceauth/services/modules/discord/discord_client/app_settings.py +++ b/allianceauth/services/modules/discord/discord_client/app_settings.py @@ -9,7 +9,6 @@ To overwrite a default set the variable in your local Django settings, e.g: from ..utils import clean_setting - DISCORD_API_BASE_URL = clean_setting( 'DISCORD_API_BASE_URL', 'https://discord.com/api/' ) diff --git a/allianceauth/services/modules/discord/discord_client/client.py b/allianceauth/services/modules/discord/discord_client/client.py index 73710270..490a7f4d 100644 --- a/allianceauth/services/modules/discord/discord_client/client.py +++ b/allianceauth/services/modules/discord/discord_client/client.py @@ -2,24 +2,21 @@ import json import logging +from collections.abc import Iterable from enum import IntEnum from hashlib import md5 from http import HTTPStatus from time import sleep -from typing import List, Optional, Set, Tuple -from collections.abc import Iterable from urllib.parse import urljoin from uuid import uuid1 import requests -from requests.exceptions import HTTPError from redis import Redis +from requests.exceptions import HTTPError +from allianceauth import __title__ as AUTH_TITLE, __url__, __version__ from allianceauth.utils.cache import get_redis_client -from allianceauth import __title__ as AUTH_TITLE -from allianceauth import __url__, __version__ - from .. import __title__ from ..utils import LoggerAddTag from .app_settings import ( @@ -347,7 +344,7 @@ class DiscordClient: Returns: List of tuple of Role and created flag """ - roles = list() + roles = [] guild_roles = RolesSet(self.guild_roles(guild_id)) role_names_cleaned = {Role.sanitize_name(name) for name in role_names} for role_name in role_names_cleaned: @@ -483,7 +480,7 @@ class DiscordClient: if role_ids and not isinstance(role_ids, list): raise TypeError('role_ids must be a list type') - data = dict() + data = {} if role_ids: data['roles'] = self._sanitize_role_ids(role_ids) @@ -639,7 +636,7 @@ class DiscordClient: uid = uuid1().hex if not hasattr(requests, method): - raise ValueError('Invalid method: %s' % method) + raise ValueError(f'Invalid method: {method}') if not authorization: authorization = f'Bot {self.access_token}' diff --git a/allianceauth/services/modules/discord/discord_client/helpers.py b/allianceauth/services/modules/discord/discord_client/helpers.py index 2932e3c5..2bcca64d 100644 --- a/allianceauth/services/modules/discord/discord_client/helpers.py +++ b/allianceauth/services/modules/discord/discord_client/helpers.py @@ -1,6 +1,5 @@ -from copy import copy -from typing import List, Optional, Set, Tuple from collections.abc import Iterable +from copy import copy from .models import Role @@ -17,19 +16,19 @@ class RolesSet: roles_lst: List of dicts, each defining a role """ def __init__(self, roles_lst: Iterable[Role]) -> None: - if not isinstance(roles_lst, (list, set, tuple)): + if not isinstance(roles_lst, list | set | tuple): raise TypeError('roles_lst must be of type list, set or tuple') - self._roles = dict() - self._roles_by_name = dict() + self._roles = {} + self._roles_by_name = {} for role in list(roles_lst): if not isinstance(role, Role): - raise TypeError('Roles must be of type Role: %s' % role) + raise TypeError(f'Roles must be of type Role: {role}') self._roles[role.id] = role self._roles_by_name[role.name] = role def __repr__(self) -> str: if self._roles_by_name: - roles = '"' + '", "'.join(sorted(list(self._roles_by_name.keys()))) + '"' + roles = '"' + '", "'.join(sorted(self._roles_by_name.keys())) + '"' else: roles = "" return f'{self.__class__.__name__}([{roles}])' diff --git a/allianceauth/services/modules/discord/discord_client/models.py b/allianceauth/services/modules/discord/discord_client/models.py index 2f8f230a..f38d3ac6 100644 --- a/allianceauth/services/modules/discord/discord_client/models.py +++ b/allianceauth/services/modules/discord/discord_client/models.py @@ -7,7 +7,6 @@ Discord's snowflake type (used by Discord IDs) is implemented as int. """ from dataclasses import asdict, dataclass -from typing import FrozenSet @dataclass(frozen=True) diff --git a/allianceauth/services/modules/discord/discord_client/tests/factories.py b/allianceauth/services/modules/discord/discord_client/tests/factories.py index a11548bf..4f63bb92 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/factories.py +++ b/allianceauth/services/modules/discord/discord_client/tests/factories.py @@ -103,7 +103,7 @@ def next_number(key: str = None) -> int: try: return next_number._counter[key].__next__() except AttributeError: - next_number._counter = dict() + next_number._counter = {} except KeyError: pass next_number._counter[key] = count(start=1) diff --git a/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py b/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py index b3202d66..8d9b73da 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py +++ b/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py @@ -14,14 +14,14 @@ alliance Discord server for this. """ import os -from random import random import threading +from random import random from time import sleep + from django.test import TestCase -from .. import DiscordClient, DiscordApiBackoff - from ...utils import set_logger_to_file +from .. import DiscordApiBackoff, DiscordClient logger = set_logger_to_file( 'allianceauth.services.modules.discord.discord_client.client', __file__ diff --git a/allianceauth/services/modules/discord/discord_client/tests/piloting_functionality.py b/allianceauth/services/modules/discord/discord_client/tests/piloting_functionality.py index ad729b8b..dd74cee2 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/piloting_functionality.py +++ b/allianceauth/services/modules/discord/discord_client/tests/piloting_functionality.py @@ -14,13 +14,13 @@ Since this may cause lots of 429s we'd recommend NOT to use your alliance Discord server for this. """ -from uuid import uuid1 import os -from unittest import TestCase from time import sleep +from unittest import TestCase +from uuid import uuid1 -from .. import DiscordClient from ...utils import set_logger_to_file +from .. import DiscordClient logger = set_logger_to_file( 'allianceauth.services.modules.discord.discord_self.client.client', __file__ diff --git a/allianceauth/services/modules/discord/discord_client/tests/test_client.py b/allianceauth/services/modules/discord/discord_client/tests/test_client.py index 5c7a5bf4..044cb97a 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/test_client.py +++ b/allianceauth/services/modules/discord/discord_client/tests/test_client.py @@ -7,8 +7,7 @@ import requests_mock from redis import Redis from requests.exceptions import HTTPError -from allianceauth import __title__ as AUTH_TITLE -from allianceauth import __url__, __version__ +from allianceauth import __title__ as AUTH_TITLE, __url__, __version__ from allianceauth.utils.testing import NoSocketsTestCase from ...utils import set_logger_to_file @@ -85,7 +84,7 @@ class TestBasicsAndHelpers(NoSocketsTestCase): def test_should_raise_error_when_trying_to_create_object_without_token(self): # when/then - with self.assertRaises(Exception): + with self.assertRaises(ValueError): DiscordClient("", mock_redis) def test_repr(self): diff --git a/allianceauth/services/modules/discord/discord_client/tests/test_exceptions.py b/allianceauth/services/modules/discord/discord_client/tests/test_exceptions.py index 25f412f3..eb3323d2 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/test_exceptions.py +++ b/allianceauth/services/modules/discord/discord_client/tests/test_exceptions.py @@ -4,7 +4,7 @@ from ..exceptions import ( DiscordApiBackoff, DiscordClientException, DiscordRateLimitExhausted, - DiscordTooManyRequestsError + DiscordTooManyRequestsError, ) diff --git a/allianceauth/services/modules/discord/managers.py b/allianceauth/services/modules/discord/managers.py index 2dbdbb9f..2b7e6ea6 100644 --- a/allianceauth/services/modules/discord/managers.py +++ b/allianceauth/services/modules/discord/managers.py @@ -16,10 +16,13 @@ from .app_settings import ( DISCORD_GUILD_ID, DISCORD_SYNC_NAMES, ) -from .core import calculate_roles_for_user, create_bot_client -from .core import group_to_role as core_group_to_role -from .core import server_name as core_server_name -from .core import user_formatted_nick +from .core import ( + calculate_roles_for_user, + create_bot_client, + group_to_role as core_group_to_role, + server_name as core_server_name, + user_formatted_nick, +) from .discord_client import ( DISCORD_OAUTH_BASE_URL, DISCORD_OAUTH_TOKEN_URL, @@ -175,7 +178,7 @@ class DiscordUserManager(models.Manager): - empty dict if no matching role found """ role = core_group_to_role(group) - return role.asdict() if role else dict() + return role.asdict() if role else {} @staticmethod def server_name(use_cache: bool = True) -> str: diff --git a/allianceauth/services/modules/discord/migrations/0001_initial.py b/allianceauth/services/modules/discord/migrations/0001_initial.py index 932bb5fc..5241dbdd 100644 --- a/allianceauth/services/modules/discord/migrations/0001_initial.py +++ b/allianceauth/services/modules/discord/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:14 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/discord/migrations/0002_service_permissions.py b/allianceauth/services/modules/discord/migrations/0002_service_permissions.py index f10a1cf6..9592cbd4 100644 --- a/allianceauth/services/modules/discord/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/discord/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/discord/migrations/0003_big_overhaul.py b/allianceauth/services/modules/discord/migrations/0003_big_overhaul.py index b809fd6b..f63ab2db 100644 --- a/allianceauth/services/modules/discord/migrations/0003_big_overhaul.py +++ b/allianceauth/services/modules/discord/migrations/0003_big_overhaul.py @@ -1,8 +1,8 @@ # Generated by Django 2.2.12 on 2020-05-10 19:59 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/discord/models.py b/allianceauth/services/modules/discord/models.py index 4d74b247..a3b0966c 100644 --- a/allianceauth/services/modules/discord/models.py +++ b/allianceauth/services/modules/discord/models.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from requests.exceptions import HTTPError @@ -11,17 +10,11 @@ from allianceauth.notifications import notify from . import __title__ from .app_settings import DISCORD_GUILD_ID -from .core import ( - create_bot_client, - default_bot_client, - calculate_roles_for_user, - user_formatted_nick -) +from .core import calculate_roles_for_user, create_bot_client, default_bot_client, user_formatted_nick from .discord_client import DiscordApiBackoff from .managers import DiscordUserManager from .utils import LoggerAddTag - logger = LoggerAddTag(logging.getLogger(__name__), __title__) diff --git a/allianceauth/services/modules/discord/tasks.py b/allianceauth/services/modules/discord/tasks.py index 1dcc087e..15e6319a 100644 --- a/allianceauth/services/modules/discord/tasks.py +++ b/allianceauth/services/modules/discord/tasks.py @@ -1,7 +1,7 @@ import logging from typing import Any -from celery import shared_task, chain +from celery import chain, shared_task from requests.exceptions import HTTPError from django.contrib.auth.models import User @@ -10,14 +10,11 @@ from django.db.models.query import QuerySet from allianceauth.services.tasks import QueueOnce from . import __title__ -from .app_settings import ( - DISCORD_TASKS_MAX_RETRIES, DISCORD_TASKS_RETRY_PAUSE, DISCORD_SYNC_NAMES -) +from .app_settings import DISCORD_SYNC_NAMES, DISCORD_TASKS_MAX_RETRIES, DISCORD_TASKS_RETRY_PAUSE from .discord_client import DiscordApiBackoff from .models import DiscordUser from .utils import LoggerAddTag - logger = LoggerAddTag(logging.getLogger(__name__), __title__) # task priority of bulk tasks @@ -152,7 +149,7 @@ def _bulk_update_groups_for_users(discord_users_qs: QuerySet) -> None: logger.info( "Starting to bulk update discord roles for %d users", discord_users_qs.count() ) - update_groups_chain = list() + update_groups_chain = [] for discord_user in discord_users_qs: update_groups_chain.append(update_groups.si(discord_user.user.pk)) @@ -180,7 +177,7 @@ def _bulk_update_nicknames_for_users(discord_users_qs: QuerySet) -> None: "Starting to bulk update discord nicknames for %d users", discord_users_qs.count() ) - update_nicknames_chain = list() + update_nicknames_chain = [] for discord_user in discord_users_qs: update_nicknames_chain.append(update_nickname.si(discord_user.user.pk)) @@ -257,7 +254,7 @@ def _bulk_update_usernames_for_users(discord_users_qs: QuerySet) -> None: "Starting to bulk update discord usernames for %d users", discord_users_qs.count() ) - update_usernames_chain = list() + update_usernames_chain = [] for discord_user in discord_users_qs: update_usernames_chain.append(update_username.si(discord_user.user.pk)) @@ -271,7 +268,7 @@ def update_all() -> None: logger.info( 'Starting to bulk update all for %s Discord users', discord_users_qs.count() ) - update_all_chain = list() + update_all_chain = [] for discord_user in discord_users_qs: update_all_chain.append(update_groups.si(discord_user.user.pk)) update_all_chain.append(update_username.si(discord_user.user.pk)) diff --git a/allianceauth/services/modules/discord/tests/test_admin.py b/allianceauth/services/modules/discord/tests/test_admin.py index 03de74bd..7b236ac4 100644 --- a/allianceauth/services/modules/discord/tests/test_admin.py +++ b/allianceauth/services/modules/discord/tests/test_admin.py @@ -188,17 +188,15 @@ class TestColumnRendering(TestDataMixin, NoSocketsTestCase): def test_user_username_u1(self): expected = ( - '' - 'Bruce_Wayne
Bruce Wayne'.format( - self.user_1.discord.pk - ) + f'' + 'Bruce_Wayne
Bruce Wayne' ) self.assertEqual(user_username(self.user_1.discord), expected) def test_user_username_u3(self): expected = ( - '' - 'Lex_Luthor'.format(self.user_3.discord.pk) + f'' + 'Lex_Luthor' ) self.assertEqual(user_username(self.user_3.discord), expected) diff --git a/allianceauth/services/modules/discord/tests/test_integration.py b/allianceauth/services/modules/discord/tests/test_integration.py index 7fd9a954..fa869245 100644 --- a/allianceauth/services/modules/discord/tests/test_integration.py +++ b/allianceauth/services/modules/discord/tests/test_integration.py @@ -11,12 +11,12 @@ from unittest.mock import Mock, patch from uuid import uuid1 import requests_mock +from django_webtest import WebTest from requests.exceptions import HTTPError from django.contrib.auth.models import Group, User from django.test import TransactionTestCase, override_settings from django.urls import reverse -from django_webtest import WebTest from allianceauth.authentication.models import State from allianceauth.eveonline.models import EveCharacter @@ -224,7 +224,7 @@ class TestServiceFeatures(TransactionTestCase): requests_made = [ DiscordRequest(r.method, r.url) for r in requests_mocker.request_history ] - self.assertListEqual(requests_made, list()) + self.assertListEqual(requests_made, []) def test_when_member_is_demoted_to_guest_then_his_account_is_deleted( self, requests_mocker @@ -428,12 +428,12 @@ class StateTestCase(NoSocketsTestCase): self._refresh_user() self.assertEqual(higher_state, self.user.profile.state) with self.assertRaises(DiscordUser.DoesNotExist): - self.user.discord + _ = self.user.discord higher_state.member_characters.clear() self._refresh_user() self.assertEqual(self.member_state, self.user.profile.state) with self.assertRaises(DiscordUser.DoesNotExist): - self.user.discord + _ = self.user.discord def test_perm_changes_to_lower_priority_state_creation(self, requests_mocker): mock_url = DiscordRequest( @@ -455,12 +455,12 @@ class StateTestCase(NoSocketsTestCase): self._refresh_user() self.assertEqual(lower_state, self.user.profile.state) with self.assertRaises(DiscordUser.DoesNotExist): - self.user.discord + _ = self.user.discord self.member_state.member_characters.add(self.test_character) self._refresh_user() self.assertEqual(self.member_state, self.user.profile.state) with self.assertRaises(DiscordUser.DoesNotExist): - self.user.discord + _ = self.user.discord @patch(MODULE_PATH + '.core.DISCORD_GUILD_ID', TEST_GUILD_ID) @@ -534,7 +534,7 @@ class TestUserFeatures(WebTest): self.assertTrue(mock_messages.success.called) self.assertFalse(mock_messages.error.called) - requests_made = list() + requests_made = [] for r in requests_mocker.request_history: obj = DiscordRequest(r.method, r.url) requests_made.append(obj) @@ -662,7 +662,7 @@ class TestUserFeatures(WebTest): self.assertFalse(mock_messages.success.called) self.assertTrue(mock_messages.error.called) - requests_made = list() + requests_made = [] for r in requests_mocker.request_history: obj = DiscordRequest(r.method, r.url) requests_made.append(obj) @@ -694,7 +694,7 @@ class TestUserFeatures(WebTest): self.assertTrue(mock_messages.success.called) self.assertFalse(mock_messages.error.called) - requests_made = list() + requests_made = [] for r in requests_mocker.request_history: obj = DiscordRequest(r.method, r.url) requests_made.append(obj) @@ -730,7 +730,7 @@ class TestUserFeatures(WebTest): self.assertFalse(mock_messages.success.called) self.assertTrue(mock_messages.error.called) - requests_made = list() + requests_made = [] for r in requests_mocker.request_history: obj = DiscordRequest(r.method, r.url) requests_made.append(obj) diff --git a/allianceauth/services/modules/discord/tests/test_managers.py b/allianceauth/services/modules/discord/tests/test_managers.py index 8b65fd26..b081c3e9 100644 --- a/allianceauth/services/modules/discord/tests/test_managers.py +++ b/allianceauth/services/modules/discord/tests/test_managers.py @@ -3,8 +3,6 @@ from unittest.mock import Mock, patch from requests.exceptions import HTTPError -from django.contrib.auth.models import User - from allianceauth.tests.auth_utils import AuthUtils from allianceauth.utils.testing import NoSocketsTestCase diff --git a/allianceauth/services/modules/discord/tests/test_models.py b/allianceauth/services/modules/discord/tests/test_models.py index c7e73218..3e919356 100644 --- a/allianceauth/services/modules/discord/tests/test_models.py +++ b/allianceauth/services/modules/discord/tests/test_models.py @@ -11,8 +11,8 @@ from ..discord_client.tests.factories import ( TEST_USER_NAME, create_guild_member, create_role, + create_user as create_guild_user, ) -from ..discord_client.tests.factories import create_user as create_guild_user from ..models import DiscordUser from ..utils import set_logger_to_file from . import MODULE_PATH, TEST_MAIN_ID, TEST_MAIN_NAME diff --git a/allianceauth/services/modules/discord/utils.py b/allianceauth/services/modules/discord/utils.py index 26ffbe00..79a5525a 100644 --- a/allianceauth/services/modules/discord/utils.py +++ b/allianceauth/services/modules/discord/utils.py @@ -3,7 +3,6 @@ import os from django.conf import settings - logger = logging.getLogger(__name__) @@ -41,7 +40,7 @@ def clean_setting( if not required_type: required_type = type(default_value) - if min_value is None and required_type == int: + if min_value is None and required_type is int: min_value = 0 if not hasattr(settings, name): diff --git a/allianceauth/services/modules/discord/views.py b/allianceauth/services/modules/discord/views.py index 8cc5fcdd..8a4272fd 100644 --- a/allianceauth/services/modules/discord/views.py +++ b/allianceauth/services/modules/discord/views.py @@ -1,9 +1,7 @@ import logging from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required -from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.decorators import login_required, permission_required, user_passes_test from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ @@ -13,7 +11,6 @@ from . import __title__ from .models import DiscordUser from .utils import LoggerAddTag - logger = LoggerAddTag(logging.getLogger(__name__), __title__) ACCESS_PERM = 'discord.access_discord' diff --git a/allianceauth/services/modules/discourse/admin.py b/allianceauth/services/modules/discourse/admin.py index b4efc18b..b29bdbbe 100644 --- a/allianceauth/services/modules/discourse/admin.py +++ b/allianceauth/services/modules/discourse/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import DiscourseUser from ...admin import ServicesUserAdmin +from .models import DiscourseUser @admin.register(DiscourseUser) diff --git a/allianceauth/services/modules/discourse/auth_hooks.py b/allianceauth/services/modules/discourse/auth_hooks.py index 744dbe4c..66b2553e 100644 --- a/allianceauth/services/modules/discourse/auth_hooks.py +++ b/allianceauth/services/modules/discourse/auth_hooks.py @@ -1,10 +1,11 @@ import logging -from django.template.loader import render_to_string from django.conf import settings +from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import DiscourseTasks from .urls import urlpatterns @@ -35,7 +36,7 @@ class DiscourseService(ServicesHook): self.delete_user(user, notify_user=True) def update_all_groups(self): - logger.debug('Update all %s groups called' % self.name) + logger.debug(f'Update all {self.name} groups called') DiscourseTasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/discourse/manager.py b/allianceauth/services/modules/discourse/manager.py index 03cbbdee..95f9084d 100644 --- a/allianceauth/services/modules/discourse/manager.py +++ b/allianceauth/services/modules/discourse/manager.py @@ -1,10 +1,12 @@ import logging -import requests import re +from hashlib import md5 + from django.conf import settings from django.core.cache import cache -from hashlib import md5 + from . import providers + logger = logging.getLogger(__name__) GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours @@ -39,11 +41,11 @@ class DiscourseManager: @staticmethod def _generate_cache_group_name_key(name): - return 'DISCOURSE_GROUP_NAME__%s' % md5(name.encode('utf-8')).hexdigest() + return 'DISCOURSE_GROUP_NAME__{}'.format(md5(name.encode('utf-8')).hexdigest()) @staticmethod def _generate_cache_group_id_key(g_id): - return 'DISCOURSE_GROUP_ID__%s' % g_id + return f'DISCOURSE_GROUP_ID__{g_id}' @staticmethod def __group_name_to_id(name): @@ -66,7 +68,7 @@ class DiscourseManager: for g in groups: if g['id'] == g_id: return g['name'] - raise KeyError("Group ID %s not found on Discourse" % g_id) + raise KeyError(f"Group ID {g_id} not found on Discourse") return cache.get_or_set(DiscourseManager._generate_cache_group_id_key(g_id), get_group_name, GROUP_CACHE_MAX_AGE) @@ -183,7 +185,7 @@ class DiscourseManager: username = discord_user['username'] uid = discord_user['id'] user_groups = DiscourseManager.__get_user_groups(username) - add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups] + add_groups = [group_dict[x] for x in group_dict if group_dict[x] not in user_groups] rem_groups = [x for x in user_groups if x not in inv_group_dict] if add_groups: logger.info( @@ -198,8 +200,8 @@ class DiscourseManager: @staticmethod def disable_user(user): - logger.debug("Disabling user %s Discourse access." % user) + logger.debug(f"Disabling user {user} Discourse access.") d_user = DiscourseManager.__get_user_by_external(user.pk) DiscourseManager.__logout(d_user['user']['id']) - logger.info("Disabled user %s Discourse access." % user) + logger.info(f"Disabled user {user} Discourse access.") return True diff --git a/allianceauth/services/modules/discourse/migrations/0001_initial.py b/allianceauth/services/modules/discourse/migrations/0001_initial.py index f9e0139c..74cd7be3 100644 --- a/allianceauth/services/modules/discourse/migrations/0001_initial.py +++ b/allianceauth/services/modules/discourse/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:15 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/discourse/migrations/0002_service_permissions.py b/allianceauth/services/modules/discourse/migrations/0002_service_permissions.py index bde79988..2e3e3fee 100644 --- a/allianceauth/services/modules/discourse/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/discourse/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/discourse/models.py b/allianceauth/services/modules/discourse/models.py index 22014cf9..8f121c81 100644 --- a/allianceauth/services/modules/discourse/models.py +++ b/allianceauth/services/modules/discourse/models.py @@ -9,10 +9,12 @@ class DiscourseUser(models.Model): related_name='discourse') enabled = models.BooleanField() - def __str__(self): - return self.user.username + class Meta: permissions = ( ("access_discourse", "Can access the Discourse service"), ) + + def __str__(self): + return self.user.username diff --git a/allianceauth/services/modules/discourse/providers.py b/allianceauth/services/modules/discourse/providers.py index 276d82da..39f45c99 100644 --- a/allianceauth/services/modules/discourse/providers.py +++ b/allianceauth/services/modules/discourse/providers.py @@ -1,7 +1,9 @@ from pydiscourse import DiscourseClient + from django.conf import settings -class DiscourseAPIClient(): + +class DiscourseAPIClient: _client = None def __init__(self): diff --git a/allianceauth/services/modules/discourse/tasks.py b/allianceauth/services/modules/discourse/tasks.py index d115211a..48487e1f 100644 --- a/allianceauth/services/modules/discourse/tasks.py +++ b/allianceauth/services/modules/discourse/tasks.py @@ -1,12 +1,14 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from celery import shared_task from allianceauth.notifications import notify from allianceauth.services.hooks import NameFormatter from allianceauth.services.tasks import QueueOnce + from .manager import DiscourseManager from .models import DiscourseUser @@ -20,7 +22,7 @@ class DiscourseTasks: @classmethod def delete_user(cls, user, notify_user=False): if cls.has_account(user) and user.discourse.enabled: - logger.debug("User %s has a Discourse account. Disabling login." % user) + logger.debug(f"User {user} has a Discourse account. Disabling login.") if DiscourseManager.disable_user(user): user.discourse.delete() if notify_user: @@ -44,14 +46,14 @@ class DiscourseTasks: @shared_task(bind=True, name='discourse.update_groups', base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating discourse groups for user %s" % user) + logger.debug(f"Updating discourse groups for user {user}") try: DiscourseManager.update_groups(user) except Exception as e: logger.exception(e) - logger.warning("Discourse group sync failed for %s, retrying in 10 mins" % user) + logger.warning(f"Discourse group sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s discourse groups." % user) + logger.debug(f"Updated user {user} discourse groups.") @staticmethod @shared_task(name='discourse.update_all_groups') diff --git a/allianceauth/services/modules/discourse/tests.py b/allianceauth/services/modules/discourse/tests.py index 38eda84f..19d7c361 100644 --- a/allianceauth/services/modules/discourse/tests.py +++ b/allianceauth/services/modules/discourse/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory -from django.contrib.auth.models import User, Group, Permission -from django.core.exceptions import ObjectDoesNotExist from django.conf import settings +from django.contrib.auth.models import Group, Permission, User +from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class DiscourseHooksTestCase(TestCase): member = AuthUtils.create_member(self.member) DiscourseUser.objects.create(user=member, enabled=True) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = DiscourseService add_permissions() @@ -85,7 +85,7 @@ class DiscourseHooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - none_discourse = User.objects.get(username=self.none_user).discourse + _ = User.objects.get(username=self.none_user).discourse @mock.patch(MODULE_PATH + '.tasks.DiscourseManager') def test_delete_user(self, manager): @@ -97,7 +97,7 @@ class DiscourseHooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - discourse_user = User.objects.get(username=self.member).discourse + _ = User.objects.get(username=self.member).discourse def test_render_services_ctrl(self): service = self.service() @@ -107,7 +107,7 @@ class DiscourseHooksTestCase(TestCase): response = service.render_services_ctrl(request) self.assertTemplateUsed(service.service_ctrl_template) - self.assertIn('href="%s"' % settings.DISCOURSE_URL, response) + self.assertIn(f'href="{settings.DISCOURSE_URL}"', response) class DiscourseViewsTestCase(TestCase): diff --git a/allianceauth/services/modules/discourse/views.py b/allianceauth/services/modules/discourse/views.py index a777fbb7..3e42b2f4 100644 --- a/allianceauth/services/modules/discourse/views.py +++ b/allianceauth/services/modules/discourse/views.py @@ -1,20 +1,18 @@ +import base64 +import hashlib +import hmac +import logging +from urllib.parse import parse_qs, unquote, urlencode + from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ from .manager import DiscourseManager -from .tasks import DiscourseTasks from .models import DiscourseUser - -import base64 -import hmac -import hashlib - -from urllib.parse import unquote, urlencode, parse_qs - -import logging +from .tasks import DiscourseTasks logger = logging.getLogger(__name__) @@ -27,12 +25,12 @@ def discourse_sso(request): # Check if user has access if not request.user.has_perm(ACCESS_PERM): messages.error(request, _('You are not authorized to access Discourse.')) - logger.warning('User %s attempted to access Discourse but does not have permission.' % request.user) + logger.warning(f'User {request.user} attempted to access Discourse but does not have permission.') return redirect('authentication:dashboard') if not request.user.profile.main_character: messages.error(request, _("You must have a main character set to access Discourse.")) - logger.warning('User %s attempted to access Discourse but does not have a main character.' % request.user) + logger.warning(f'User {request.user} attempted to access Discourse but does not have a main character.') return redirect('authentication:characters') main_char = request.user.profile.main_character @@ -92,5 +90,5 @@ def discourse_sso(request): DiscourseTasks.update_groups.apply_async(args=[request.user.pk], countdown=30) # Redirect back to Discourse - url = '%s/session/sso_login' % settings.DISCOURSE_URL + url = f'{settings.DISCOURSE_URL}/session/sso_login' return redirect(f'{url}?{query_string}') diff --git a/allianceauth/services/modules/example/auth_hooks.py b/allianceauth/services/modules/example/auth_hooks.py index 5469b3af..bc2c7b0f 100644 --- a/allianceauth/services/modules/example/auth_hooks.py +++ b/allianceauth/services/modules/example/auth_hooks.py @@ -2,6 +2,7 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .urls import urlpatterns diff --git a/allianceauth/services/modules/ips4/admin.py b/allianceauth/services/modules/ips4/admin.py index 38ad5f50..3e45cea3 100644 --- a/allianceauth/services/modules/ips4/admin.py +++ b/allianceauth/services/modules/ips4/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Ips4User diff --git a/allianceauth/services/modules/ips4/auth_hooks.py b/allianceauth/services/modules/ips4/auth_hooks.py index 83a9855b..d475ab10 100644 --- a/allianceauth/services/modules/ips4/auth_hooks.py +++ b/allianceauth/services/modules/ips4/auth_hooks.py @@ -3,6 +3,7 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import Ips4Tasks from .urls import urlpatterns diff --git a/allianceauth/services/modules/ips4/manager.py b/allianceauth/services/modules/ips4/manager.py index 40b1cad9..cc14bf38 100644 --- a/allianceauth/services/modules/ips4/manager.py +++ b/allianceauth/services/modules/ips4/manager.py @@ -1,10 +1,12 @@ import logging import random -import string import re -from django.db import connections +import string + from passlib.hash import bcrypt + from django.conf import settings +from django.db import connections logger = logging.getLogger(__name__) @@ -13,17 +15,17 @@ TABLE_PREFIX = getattr(settings, 'IPS4_TABLE_PREFIX', '') class Ips4Manager: - SQL_ADD_USER = r"INSERT INTO %score_members (name, email, members_pass_hash, members_pass_salt, " \ - r"member_group_id) VALUES (%%s, %%s, %%s, %%s, %%s)" % TABLE_PREFIX - SQL_GET_ID = r"SELECT member_id FROM %score_members WHERE name = %%s" % TABLE_PREFIX - SQL_UPDATE_PASSWORD = r"UPDATE %score_members SET members_pass_hash = %%s, members_pass_salt = %%s WHERE name = %%s" % TABLE_PREFIX - SQL_DEL_USER = r"DELETE FROM %score_members WHERE member_id = %%s" % TABLE_PREFIX + SQL_ADD_USER = rf"INSERT INTO {TABLE_PREFIX}core_members (name, email, members_pass_hash, members_pass_salt, " \ + r"member_group_id) VALUES (%s, %s, %s, %s, %s)" + SQL_GET_ID = rf"SELECT member_id FROM {TABLE_PREFIX}core_members WHERE name = %s" + SQL_UPDATE_PASSWORD = rf"UPDATE {TABLE_PREFIX}core_members SET members_pass_hash = %s, members_pass_salt = %s WHERE name = %s" + SQL_DEL_USER = rf"DELETE FROM {TABLE_PREFIX}core_members WHERE member_id = %s" MEMBER_GROUP_ID = 3 @classmethod def add_user(cls, username, email): - logger.debug("Adding new IPS4 user %s" % username) + logger.debug(f"Adding new IPS4 user {username}") plain_password = cls.__generate_random_pass() hash = cls._gen_pwhash(plain_password) salt = cls._get_salt(hash) @@ -42,7 +44,7 @@ class Ips4Manager: logger.debug(f"Got user id {row[0]} for username {username}") return row[0] else: - logger.error("username %s not found. Unable to determine id." % username) + logger.error(f"username {username} not found. Unable to determine id.") return None @staticmethod @@ -61,19 +63,19 @@ class Ips4Manager: @staticmethod def delete_user(id): - logger.debug("Deleting IPS4 user id %s" % id) + logger.debug(f"Deleting IPS4 user id {id}") try: cursor = connections['ips4'].cursor() cursor.execute(Ips4Manager.SQL_DEL_USER, [id]) - logger.info("Deleted IPS4 user %s" % id) + logger.info(f"Deleted IPS4 user {id}") return True - except: - logger.exception("Failed to delete IPS4 user id %s" % id) + except Exception: + logger.exception(f"Failed to delete IPS4 user id {id}") return False @classmethod def update_user_password(cls, username): - logger.debug("Updating IPS4 user id %s password" % id) + logger.debug(f"Updating IPS4 user id {id} password") if cls.check_user(username): plain_password = Ips4Manager.__generate_random_pass() hash = cls._gen_pwhash(plain_password) @@ -82,24 +84,24 @@ class Ips4Manager: cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username]) return plain_password else: - logger.error("Unable to update ips4 user %s password" % username) + logger.error(f"Unable to update ips4 user {username} password") return "" @staticmethod def check_user(username): - logger.debug("Checking IPS4 username %s" % username) + logger.debug(f"Checking IPS4 username {username}") cursor = connections['ips4'].cursor() cursor.execute(Ips4Manager.SQL_GET_ID, [username]) row = cursor.fetchone() if row: - logger.debug("Found user %s on IPS4" % username) + logger.debug(f"Found user {username} on IPS4") return True - logger.debug("User %s not found on IPS4" % username) + logger.debug(f"User {username} not found on IPS4") return False @classmethod def update_custom_password(cls, username, plain_password): - logger.debug("Updating IPS4 user id %s password" % id) + logger.debug(f"Updating IPS4 user id {id} password") if cls.check_user(username): hash = cls._gen_pwhash(plain_password) salt = cls._get_salt(hash) @@ -107,5 +109,5 @@ class Ips4Manager: cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username]) return plain_password else: - logger.error("Unable to update ips4 user %s password" % username) + logger.error(f"Unable to update ips4 user {username} password") return "" diff --git a/allianceauth/services/modules/ips4/migrations/0001_initial.py b/allianceauth/services/modules/ips4/migrations/0001_initial.py index 83f3159f..adb8b3a5 100644 --- a/allianceauth/services/modules/ips4/migrations/0001_initial.py +++ b/allianceauth/services/modules/ips4/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:27 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/ips4/migrations/0002_service_permissions.py b/allianceauth/services/modules/ips4/migrations/0002_service_permissions.py index 17c0fc09..b184f88b 100644 --- a/allianceauth/services/modules/ips4/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/ips4/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/ips4/models.py b/allianceauth/services/modules/ips4/models.py index 56d83263..545a107c 100644 --- a/allianceauth/services/modules/ips4/models.py +++ b/allianceauth/services/modules/ips4/models.py @@ -10,10 +10,11 @@ class Ips4User(models.Model): username = models.CharField(max_length=254) id = models.CharField(max_length=254) - def __str__(self): - return self.username class Meta: permissions = ( ("access_ips4", "Can access the IPS4 service"), ) + + def __str__(self): + return self.username diff --git a/allianceauth/services/modules/ips4/tasks.py b/allianceauth/services/modules/ips4/tasks.py index b6c5dce8..3cb4d1d7 100644 --- a/allianceauth/services/modules/ips4/tasks.py +++ b/allianceauth/services/modules/ips4/tasks.py @@ -1,12 +1,12 @@ -from django.conf import settings +import logging + from django.core.exceptions import ObjectDoesNotExist from allianceauth.services.hooks import NameFormatter + from .manager import Ips4Manager from .models import Ips4User -import logging - logger = logging.getLogger(__name__) @@ -16,10 +16,10 @@ class Ips4Tasks: @classmethod def delete_user(cls, user): - logging.debug("Attempting to delete IPS4 account for %s" % user) + logging.debug(f"Attempting to delete IPS4 account for {user}") if cls.has_account(user) and Ips4Manager.delete_user(user.ips4.id): user.ips4.delete() - logger.info("Successfully deactivated IPS4 for user %s" % user) + logger.info(f"Successfully deactivated IPS4 for user {user}") return True return False diff --git a/allianceauth/services/modules/ips4/tests.py b/allianceauth/services/modules/ips4/tests.py index 48e7ffa4..09f2484e 100644 --- a/allianceauth/services/modules/ips4/tests.py +++ b/allianceauth/services/modules/ips4/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class Ips4HooksTestCase(TestCase): member = AuthUtils.create_member(self.member) Ips4User.objects.create(user=member, id='12345', username=self.member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = Ips4Service add_permissions() @@ -107,7 +107,8 @@ class Ips4ViewsTestCase(TestCase): self.assertTrue(manager.delete_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - ips4_user = User.objects.get(pk=self.member.pk).ips4 + user = User.objects.get(pk=self.member.pk) + _ = user.ips4 @mock.patch(MODULE_PATH + '.views.Ips4Manager') def test_set_password(self, manager): diff --git a/allianceauth/services/modules/ips4/views.py b/allianceauth/services/modules/ips4/views.py index 9e806430..ea1b6284 100644 --- a/allianceauth/services/modules/ips4/views.py +++ b/allianceauth/services/modules/ips4/views.py @@ -2,10 +2,11 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from allianceauth.services.forms import ServicePasswordForm + from .manager import Ips4Manager from .models import Ips4User from .tasks import Ips4Tasks @@ -18,16 +19,16 @@ ACCESS_PERM = 'ips4.access_ips4' @login_required @permission_required(ACCESS_PERM) def activate_ips4(request): - logger.debug("activate_ips4 called by user %s" % request.user) + logger.debug(f"activate_ips4 called by user {request.user}") character = request.user.profile.main_character logger.debug(f"Adding IPS4 user for user {request.user} with main character {character}") result = Ips4Manager.add_user(Ips4Tasks.get_username(request.user), request.user.email) # if empty we failed if result[0] != "" and not Ips4Tasks.has_account(request.user): - ips_user = Ips4User.objects.create(user=request.user, id=result[2], username=result[0]) - logger.debug("Updated authserviceinfo for user %s with IPSuite4 credentials." % request.user) + Ips4User.objects.create(user=request.user, id=result[2], username=result[0]) + logger.debug(f"Updated authserviceinfo for user {request.user} with IPSuite4 credentials.") # update_ips4_groups.delay(request.user.pk) - logger.info("Successfully activated IPSuite4 for user %s" % request.user) + logger.info(f"Successfully activated IPSuite4 for user {request.user}") messages.success(request, _('Activated IPSuite4 account.')) credentials = { 'username': result[0], @@ -35,7 +36,7 @@ def activate_ips4(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'IPSuite4'}) else: - logger.error("Unsuccessful attempt to activate IPSuite4 for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate IPSuite4 for user {request.user}") messages.error(request, _('An error occurred while processing your IPSuite4 account.')) return redirect("services:services") @@ -43,12 +44,12 @@ def activate_ips4(request): @login_required @permission_required(ACCESS_PERM) def reset_ips4_password(request): - logger.debug("reset_ips4_password called by user %s" % request.user) + logger.debug(f"reset_ips4_password called by user {request.user}") if Ips4Tasks.has_account(request.user): result = Ips4Manager.update_user_password(request.user.ips4.username) # false we failed if result != "": - logger.info("Successfully reset IPSuite4 password for user %s" % request.user) + logger.info(f"Successfully reset IPSuite4 password for user {request.user}") messages.success(request, _('Reset IPSuite4 password.')) credentials = { 'username': request.user.ips4.username, @@ -56,7 +57,7 @@ def reset_ips4_password(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'IPSuite4'}) - logger.error("Unsuccessful attempt to reset IPSuite4 password for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset IPSuite4 password for user {request.user}") messages.error(request, _('An error occurred while processing your IPSuite4 account.')) return redirect("services:services") @@ -64,27 +65,27 @@ def reset_ips4_password(request): @login_required @permission_required(ACCESS_PERM) def set_ips4_password(request): - logger.debug("set_ips4_password called by user %s" % request.user) + logger.debug(f"set_ips4_password called by user {request.user}") if request.method == 'POST': logger.debug("Received POST request with form.") form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) + logger.debug(f"Form is valid: {form.is_valid()}") if form.is_valid() and Ips4Tasks.has_account(request.user): password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) + logger.debug(f"Form contains password of length {len(password)}") result = Ips4Manager.update_custom_password(request.user.ips4.username, plain_password=password) if result != "": - logger.info("Successfully set IPSuite4 password for user %s" % request.user) + logger.info(f"Successfully set IPSuite4 password for user {request.user}") messages.success(request, _('Set IPSuite4 password.')) else: - logger.error("Failed to install custom IPSuite4 password for user %s" % request.user) + logger.error(f"Failed to install custom IPSuite4 password for user {request.user}") messages.error(request, _('An error occurred while processing your IPSuite4 account.')) return redirect('services:services') else: logger.debug("Request is not type POST - providing empty form.") form = ServicePasswordForm() - logger.debug("Rendering form for user %s" % request.user) + logger.debug(f"Rendering form for user {request.user}") context = {'form': form, 'service': 'IPS4'} return render(request, 'services/service_password.html', context=context) @@ -92,11 +93,11 @@ def set_ips4_password(request): @login_required @permission_required(ACCESS_PERM) def deactivate_ips4(request): - logger.debug("deactivate_ips4 called by user %s" % request.user) + logger.debug(f"deactivate_ips4 called by user {request.user}") if Ips4Tasks.delete_user(request.user): - logger.info("Successfully deactivated IPSuite4 for user %s" % request.user) + logger.info(f"Successfully deactivated IPSuite4 for user {request.user}") messages.success(request, _('Deactivated IPSuite4 account.')) else: - logger.error("Unsuccessful attempt to deactivate IPSuite4 for user %s" % request.user) + logger.error(f"Unsuccessful attempt to deactivate IPSuite4 for user {request.user}") messages.error(request, _('An error occurred while processing your IPSuite4 account.')) return redirect("services:services") diff --git a/allianceauth/services/modules/mumble/admin.py b/allianceauth/services/modules/mumble/admin.py index ecc8328d..33034d30 100644 --- a/allianceauth/services/modules/mumble/admin.py +++ b/allianceauth/services/modules/mumble/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import MumbleUser from ...admin import ServicesUserAdmin +from .models import MumbleUser @admin.register(MumbleUser) diff --git a/allianceauth/services/modules/mumble/auth_hooks.py b/allianceauth/services/modules/mumble/auth_hooks.py index efc8df33..3bf3d25e 100644 --- a/allianceauth/services/modules/mumble/auth_hooks.py +++ b/allianceauth/services/modules/mumble/auth_hooks.py @@ -3,12 +3,13 @@ import urllib from django.conf import settings from django.template.loader import render_to_string -from allianceauth.notifications import notify from allianceauth import hooks +from allianceauth.notifications import notify from allianceauth.services.hooks import ServicesHook -from .tasks import MumbleTasks + from .models import MumbleUser +from .tasks import MumbleTasks from .urls import urlpatterns logger = logging.getLogger(__name__) @@ -50,7 +51,7 @@ class MumbleService(ServicesHook): self.delete_user(user, notify_user=True) def update_all_groups(self): - logger.debug("Updating all %s groups" % self.name) + logger.debug(f"Updating all {self.name} groups") MumbleTasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/mumble/migrations/0001_squashed_0011_auto_20201011_1009.py b/allianceauth/services/modules/mumble/migrations/0001_squashed_0011_auto_20201011_1009.py index 4ef869f9..6b6d96f6 100644 --- a/allianceauth/services/modules/mumble/migrations/0001_squashed_0011_auto_20201011_1009.py +++ b/allianceauth/services/modules/mumble/migrations/0001_squashed_0011_auto_20201011_1009.py @@ -1,9 +1,10 @@ # Generated by Django 3.1.2 on 2021-01-08 13:54 -from django.conf import settings -from django.db import migrations, models import django.db.migrations.operations.special import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/mumble/migrations/0003_mumbleuser_user.py b/allianceauth/services/modules/mumble/migrations/0003_mumbleuser_user.py index 9745e67e..f5f98351 100644 --- a/allianceauth/services/modules/mumble/migrations/0003_mumbleuser_user.py +++ b/allianceauth/services/modules/mumble/migrations/0003_mumbleuser_user.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:31 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/mumble/migrations/0004_auto_20161214_1024.py b/allianceauth/services/modules/mumble/migrations/0004_auto_20161214_1024.py index b74ebd46..20ec8679 100644 --- a/allianceauth/services/modules/mumble/migrations/0004_auto_20161214_1024.py +++ b/allianceauth/services/modules/mumble/migrations/0004_auto_20161214_1024.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.4 on 2016-12-14 10:24 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/mumble/migrations/0006_service_permissions.py b/allianceauth/services/modules/mumble/migrations/0006_service_permissions.py index 1b64ecc9..171915c1 100644 --- a/allianceauth/services/modules/mumble/migrations/0006_service_permissions.py +++ b/allianceauth/services/modules/mumble/migrations/0006_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/mumble/migrations/0007_not_null_user.py b/allianceauth/services/modules/mumble/migrations/0007_not_null_user.py index df12bd95..14d4bd03 100644 --- a/allianceauth/services/modules/mumble/migrations/0007_not_null_user.py +++ b/allianceauth/services/modules/mumble/migrations/0007_not_null_user.py @@ -1,8 +1,8 @@ # Generated by Django 1.11.6 on 2017-10-09 09:19 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py b/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py index 4a4a679d..5d89878e 100644 --- a/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py +++ b/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py @@ -2,6 +2,7 @@ from django.db import migrations, models + class Migration(migrations.Migration): dependencies = [ diff --git a/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py b/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py index c81f54dc..4048f61d 100644 --- a/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py +++ b/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py @@ -1,7 +1,10 @@ from django.db import migrations, models -from ..auth_hooks import MumbleService + from allianceauth.services.hooks import NameFormatter +from ..auth_hooks import MumbleService + + def fwd_func(apps, schema_editor): MumbleUser = apps.get_model("mumble", "MumbleUser") db_alias = schema_editor.connection.alias diff --git a/allianceauth/services/modules/mumble/models.py b/allianceauth/services/modules/mumble/models.py index 72ab63fc..a7837689 100644 --- a/allianceauth/services/modules/mumble/models.py +++ b/allianceauth/services/modules/mumble/models.py @@ -1,12 +1,14 @@ +import logging import random import string + from passlib.hash import bcrypt_sha256 -from django.db import models from django.contrib.auth.models import Group -from allianceauth.services.hooks import NameFormatter +from django.db import models + from allianceauth.services.abstract import AbstractServiceModel -import logging +from allianceauth.services.hooks import NameFormatter logger = logging.getLogger(__name__) @@ -43,8 +45,7 @@ class MumbleManager(models.Manager): display_name = self.get_display_name(user) password = self.generate_random_pass() pwhash = self.gen_pwhash(password) - logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format( - username_clean, pwhash[0:5])) + logger.debug(f"Proceeding with mumble user creation: clean username {username_clean}, pwhash starts with {pwhash[0:5]}") logger.info(f"Creating mumble user {username_clean}") result = super().create(user=user, username=username_clean, @@ -71,12 +72,11 @@ class MumbleUser(AbstractServiceModel): username = models.CharField(max_length=254, unique=True) pwhash = models.CharField(max_length=90) hashfn = models.CharField(max_length=20, default='sha1') - groups = models.TextField(blank=True, null=True) + groups = models.TextField(blank=True) certhash = models.CharField( verbose_name="Certificate Hash", max_length=254, blank=True, - null=True, editable=False, help_text="Hash of Mumble client certificate as presented when user authenticates" ) @@ -88,7 +88,6 @@ class MumbleUser(AbstractServiceModel): verbose_name="Mumble Release", max_length=254, blank=True, - null=True, editable=False, help_text="The Mumble Release the user last authenticated with" ) @@ -123,12 +122,11 @@ class MumbleUser(AbstractServiceModel): def update_password(self, password=None): init_password = password - logger.debug(f"Updating mumble user %s password.") + logger.debug("Updating mumble user %s password.") if not password: password = MumbleManager.generate_random_pass() pwhash = MumbleManager.gen_pwhash(password) - logger.debug("Proceeding with mumble user {} password update - pwhash starts with {}".format( - self.user, pwhash[0:5])) + logger.debug(f"Proceeding with mumble user {self.user} password update - pwhash starts with {pwhash[0:5]}") self.pwhash = pwhash self.hashfn = MumbleManager.HASH_FN self.save() diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py index 5ce7c9c4..967abd9f 100644 --- a/allianceauth/services/modules/mumble/tasks.py +++ b/allianceauth/services/modules/mumble/tasks.py @@ -1,9 +1,12 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from celery import shared_task + from allianceauth.services.tasks import QueueOnce + from .models import MumbleUser logger = logging.getLogger(__name__) @@ -29,40 +32,40 @@ class MumbleTasks: @shared_task(bind=True, name="mumble.update_groups", base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating mumble groups for user %s" % user) + logger.debug(f"Updating mumble groups for user {user}") if MumbleTasks.has_account(user): try: if not user.mumble.update_groups(): raise Exception("Group sync failed") - logger.debug("Updated user %s mumble groups." % user) + logger.debug(f"Updated user {user} mumble groups.") return True except MumbleUser.DoesNotExist: logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account") - except: - logger.exception("Mumble group sync failed for %s, retrying in 10 mins" % user) + except Exception: + logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) else: - logger.debug("User %s does not have a mumble account, skipping" % user) + logger.debug(f"User {user} does not have a mumble account, skipping") return False @staticmethod @shared_task(bind=True, name="mumble.update_display_name", base=QueueOnce) def update_display_name(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating mumble groups for user %s" % user) + logger.debug(f"Updating mumble groups for user {user}") if MumbleTasks.has_account(user): try: if not user.mumble.update_display_name(): raise Exception("Display Name Sync failed") - logger.debug("Updated user %s mumble display name." % user) + logger.debug(f"Updated user {user} mumble display name.") return True except MumbleUser.DoesNotExist: logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account") - except: - logger.exception("Mumble display name sync failed for %s, retrying in 10 mins" % user) + except Exception: + logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) else: - logger.debug("User %s does not have a mumble account, skipping" % user) + logger.debug(f"User {user} does not have a mumble account, skipping") return False @staticmethod diff --git a/allianceauth/services/modules/mumble/tests.py b/allianceauth/services/modules/mumble/tests.py index e17d0267..3387938a 100644 --- a/allianceauth/services/modules/mumble/tests.py +++ b/allianceauth/services/modules/mumble/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -29,7 +29,7 @@ class MumbleHooksTestCase(TestCase): member = User.objects.get(pk=member.pk) MumbleUser.objects.create(user=member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = MumbleService add_permissions() @@ -85,7 +85,7 @@ class MumbleHooksTestCase(TestCase): MumbleUser.objects.create(user=none_user) service.validate_user(none_user) with self.assertRaises(ObjectDoesNotExist): - none_mumble = User.objects.get(username=self.none_user).mumble + _ = User.objects.get(username=self.none_user).mumble def test_delete_user(self): member = User.objects.get(username=self.member) @@ -95,7 +95,7 @@ class MumbleHooksTestCase(TestCase): self.assertTrue(result) with self.assertRaises(ObjectDoesNotExist): - mumble_user = User.objects.get(username=self.member).mumble + _ = User.objects.get(username=self.member).mumble def test_render_services_ctrl(self): service = self.service() @@ -162,7 +162,6 @@ class MumbleViewsTestCase(TestCase): self.assertIn('Member', mumble_user.groups) self.assertIn(',', mumble_user.groups) - def test_deactivate_post(self): self.login() MumbleUser.objects.create(user=self.member) @@ -171,7 +170,7 @@ class MumbleViewsTestCase(TestCase): self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - mumble_user = User.objects.get(pk=self.member.pk).mumble + _ = User.objects.get(pk=self.member.pk).mumble def test_set_password(self): self.login() diff --git a/allianceauth/services/modules/mumble/views.py b/allianceauth/services/modules/mumble/views.py index ba8904fd..cb13aca2 100644 --- a/allianceauth/services/modules/mumble/views.py +++ b/allianceauth/services/modules/mumble/views.py @@ -1,8 +1,12 @@ import logging +from allianceauth.services.abstract import ( + BaseCreatePasswordServiceAccountView, + BaseDeactivateServiceAccountView, + BaseResetPasswordServiceAccountView, + BaseSetPasswordServiceAccountView, +) from allianceauth.services.forms import ServicePasswordModelForm -from allianceauth.services.abstract import BaseCreatePasswordServiceAccountView, BaseDeactivateServiceAccountView, \ - BaseResetPasswordServiceAccountView, BaseSetPasswordServiceAccountView from .models import MumbleUser diff --git a/allianceauth/services/modules/openfire/admin.py b/allianceauth/services/modules/openfire/admin.py index 6485c6b8..1c95ca41 100644 --- a/allianceauth/services/modules/openfire/admin.py +++ b/allianceauth/services/modules/openfire/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import OpenfireUser from ...admin import ServicesUserAdmin +from .models import OpenfireUser @admin.register(OpenfireUser) diff --git a/allianceauth/services/modules/openfire/auth_hooks.py b/allianceauth/services/modules/openfire/auth_hooks.py index ed4e2b88..5b45ae82 100644 --- a/allianceauth/services/modules/openfire/auth_hooks.py +++ b/allianceauth/services/modules/openfire/auth_hooks.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from allianceauth import hooks from allianceauth.menu.hooks import MenuItemHook from allianceauth.services.hooks import ServicesHook + from .tasks import OpenfireTasks from .urls import urlpatterns @@ -41,7 +42,7 @@ class OpenfireService(ServicesHook): OpenfireTasks.update_groups.delay(user.pk) def update_all_groups(self): - logger.debug('Update all %s groups called' % self.name) + logger.debug(f'Update all {self.name} groups called') OpenfireTasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/openfire/manager.py b/allianceauth/services/modules/openfire/manager.py index 1e9215be..89573a97 100644 --- a/allianceauth/services/modules/openfire/manager.py +++ b/allianceauth/services/modules/openfire/manager.py @@ -1,14 +1,14 @@ -import re +import logging import random +import re import string from urllib.parse import urlparse import slixmpp -from django.conf import settings -from ofrestapi.users import Users as ofUsers from ofrestapi import exception +from ofrestapi.users import Users as ofUsers -import logging +from django.conf import settings logger = logging.getLogger(__name__) @@ -60,58 +60,58 @@ class OpenfireManager: @staticmethod def add_user(username): - logger.debug("Adding username %s to openfire." % username) + logger.debug(f"Adding username {username} to openfire.") try: sanitized_username = OpenfireManager.__sanitize_username(username) password = OpenfireManager.__generate_random_pass() api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api.add_user(sanitized_username, password) - logger.info("Added openfire user %s" % username) + logger.info(f"Added openfire user {username}") except exception.UserAlreadyExistsException: # User exist - logger.error("Attempting to add a user %s to openfire which already exists on server." % username) + logger.error(f"Attempting to add a user {username} to openfire which already exists on server.") return "", "" return sanitized_username, password @staticmethod def delete_user(username): - logger.debug("Deleting user %s from openfire." % username) + logger.debug(f"Deleting user {username} from openfire.") try: api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api.delete_user(username) - logger.info("Deleted user %s from openfire." % username) + logger.info(f"Deleted user {username} from openfire.") return True except exception.UserNotFoundException: - logger.error("Attempting to delete a user %s from openfire which was not found on server." % username) + logger.error(f"Attempting to delete a user {username} from openfire which was not found on server.") return False @staticmethod def lock_user(username): - logger.debug("Locking openfire user %s" % username) + logger.debug(f"Locking openfire user {username}") api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api.lock_user(username) - logger.info("Locked openfire user %s" % username) + logger.info(f"Locked openfire user {username}") @staticmethod def unlock_user(username): - logger.debug("Unlocking openfire user %s" % username) + logger.debug(f"Unlocking openfire user {username}") api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api.unlock_user(username) - logger.info("Unlocked openfire user %s" % username) + logger.info(f"Unlocked openfire user {username}") @staticmethod def update_user_pass(username, password=None): - logger.debug("Updating openfire user %s password." % username) + logger.debug(f"Updating openfire user {username} password.") try: if not password: password = OpenfireManager.__generate_random_pass() api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api.update_user(username, password=password) - logger.info("Updated openfire user %s password." % username) + logger.info(f"Updated openfire user {username} password.") return password except exception.UserNotFoundException: - logger.error("Unable to update openfire user %s password - user not found on server." % username) + logger.error(f"Unable to update openfire user {username} password - user not found on server.") return "" @classmethod @@ -161,7 +161,7 @@ class OpenfireManager: xmpp.process(block=True) message = None if xmpp.message_sent: - logger.debug("Sent jabber ping to group %s" % group_name) + logger.debug(f"Sent jabber ping to group {group_name}") return else: message = "Failed to send Openfire broadcast message." diff --git a/allianceauth/services/modules/openfire/migrations/0001_initial.py b/allianceauth/services/modules/openfire/migrations/0001_initial.py index 7d705897..30656fcb 100644 --- a/allianceauth/services/modules/openfire/migrations/0001_initial.py +++ b/allianceauth/services/modules/openfire/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:27 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/openfire/migrations/0002_service_permissions.py b/allianceauth/services/modules/openfire/migrations/0002_service_permissions.py index 7570cb4e..59a9aec9 100644 --- a/allianceauth/services/modules/openfire/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/openfire/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/openfire/models.py b/allianceauth/services/modules/openfire/models.py index 5299ed8d..672b9cb8 100644 --- a/allianceauth/services/modules/openfire/models.py +++ b/allianceauth/services/modules/openfire/models.py @@ -8,10 +8,11 @@ class OpenfireUser(models.Model): related_name='openfire') username = models.CharField(max_length=254) - def __str__(self): - return self.username + class Meta: permissions = ( ("access_openfire", "Can access the Openfire service"), ) + def __str__(self): + return self.username diff --git a/allianceauth/services/modules/openfire/tasks.py b/allianceauth/services/modules/openfire/tasks.py index 92948d62..6c246d37 100644 --- a/allianceauth/services/modules/openfire/tasks.py +++ b/allianceauth/services/modules/openfire/tasks.py @@ -1,12 +1,15 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist + from allianceauth.notifications import notify -from celery import shared_task -from allianceauth.services.tasks import QueueOnce -from allianceauth.services.modules.openfire.manager import OpenfireManager from allianceauth.services.hooks import NameFormatter +from allianceauth.services.modules.openfire.manager import OpenfireManager +from allianceauth.services.tasks import QueueOnce + from .models import OpenfireUser logger = logging.getLogger(__name__) @@ -43,7 +46,7 @@ class OpenfireTasks: @shared_task(bind=True, name="openfire.update_groups", base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating jabber groups for user %s" % user) + logger.debug(f"Updating jabber groups for user {user}") if OpenfireTasks.has_account(user): groups = [user.profile.state.name] for group in user.groups.all(): @@ -51,10 +54,10 @@ class OpenfireTasks: logger.debug(f"Updating user {user} jabber groups to {groups}") try: OpenfireManager.update_user_groups(user.openfire.username, groups) - except: - logger.exception("Jabber group sync failed for %s, retrying in 10 mins" % user) + except Exception: + logger.exception(f"Jabber group sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s jabber groups." % user) + logger.debug(f"Updated user {user} jabber groups.") else: logger.debug("User does not have an openfire account") diff --git a/allianceauth/services/modules/openfire/tests.py b/allianceauth/services/modules/openfire/tests.py index ed1c3fe3..a6bc9da9 100644 --- a/allianceauth/services/modules/openfire/tests.py +++ b/allianceauth/services/modules/openfire/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class OpenfireHooksTestCase(TestCase): member = AuthUtils.create_member(self.member) OpenfireUser.objects.create(user=member, username=self.member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = OpenfireService add_permissions() @@ -86,7 +86,7 @@ class OpenfireHooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.delete_user.called) with self.assertRaises(ObjectDoesNotExist): - none_openfire = User.objects.get(username=self.none_user).openfire + _ = User.objects.get(username=self.none_user).openfire @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') def test_delete_user(self, manager): @@ -98,7 +98,7 @@ class OpenfireHooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.delete_user.called) with self.assertRaises(ObjectDoesNotExist): - openfire_user = User.objects.get(username=self.member).openfire + _ = User.objects.get(username=self.member).openfire def test_render_services_ctrl(self): service = self.service() @@ -158,7 +158,7 @@ class OpenfireViewsTestCase(TestCase): self.assertTrue(manager.delete_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - openfire_user = User.objects.get(pk=self.member.pk).openfire + _ = User.objects.get(pk=self.member.pk).openfire @mock.patch(MODULE_PATH + '.views.OpenfireManager') def test_set_password(self, manager): diff --git a/allianceauth/services/modules/openfire/views.py b/allianceauth/services/modules/openfire/views.py index b07e12fa..c22f62ab 100644 --- a/allianceauth/services/modules/openfire/views.py +++ b/allianceauth/services/modules/openfire/views.py @@ -4,7 +4,7 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import Group -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from allianceauth.services.forms import ServicePasswordForm @@ -22,16 +22,16 @@ ACCESS_PERM = 'openfire.access_openfire' @login_required @permission_required(ACCESS_PERM) def activate_jabber(request): - logger.debug("activate_jabber called by user %s" % request.user) + logger.debug(f"activate_jabber called by user {request.user}") character = request.user.profile.main_character logger.debug(f"Adding Jabber user for user {request.user} with main character {character}") info = OpenfireManager.add_user(OpenfireTasks.get_username(request.user)) # If our username is blank means we already had a user if info[0] != "": OpenfireUser.objects.update_or_create(user=request.user, defaults={'username': info[0]}) - logger.debug("Updated authserviceinfo for user %s with Jabber credentials. Updating groups." % request.user) + logger.debug(f"Updated authserviceinfo for user {request.user} with Jabber credentials. Updating groups.") OpenfireTasks.update_groups.delay(request.user.pk) - logger.info("Successfully activated Jabber for user %s" % request.user) + logger.info(f"Successfully activated Jabber for user {request.user}") messages.success(request, _('Activated Jabber account.')) credentials = { 'username': info[0], @@ -39,7 +39,7 @@ def activate_jabber(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'Jabber'}) else: - logger.error("Unsuccessful attempt to activate Jabber for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate Jabber for user {request.user}") messages.error(request, _('An error occurred while processing your Jabber account.')) return redirect("services:services") @@ -47,12 +47,12 @@ def activate_jabber(request): @login_required @permission_required(ACCESS_PERM) def deactivate_jabber(request): - logger.debug("deactivate_jabber called by user %s" % request.user) + logger.debug(f"deactivate_jabber called by user {request.user}") if OpenfireTasks.has_account(request.user) and OpenfireTasks.delete_user(request.user): - logger.info("Successfully deactivated Jabber for user %s" % request.user) + logger.info(f"Successfully deactivated Jabber for user {request.user}") messages.success(request, 'Deactivated Jabber account.') else: - logger.error("Unsuccessful attempt to deactivate Jabber for user %s" % request.user) + logger.error(f"Unsuccessful attempt to deactivate Jabber for user {request.user}") messages.error(request, _('An error occurred while processing your Jabber account.')) return redirect("services:services") @@ -60,19 +60,19 @@ def deactivate_jabber(request): @login_required @permission_required(ACCESS_PERM) def reset_jabber_password(request): - logger.debug("reset_jabber_password called by user %s" % request.user) + logger.debug(f"reset_jabber_password called by user {request.user}") if OpenfireTasks.has_account(request.user): result = OpenfireManager.update_user_pass(request.user.openfire.username) # If our username is blank means we failed if result != "": - logger.info("Successfully reset Jabber password for user %s" % request.user) + logger.info(f"Successfully reset Jabber password for user {request.user}") messages.success(request, _('Reset Jabber password.')) credentials = { 'username': request.user.openfire.username, 'password': result, } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'Jabber'}) - logger.error("Unsuccessful attempt to reset Jabber for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset Jabber for user {request.user}") messages.error(request, _('An error occurred while processing your Jabber account.')) return redirect("services:services") @@ -80,7 +80,7 @@ def reset_jabber_password(request): @login_required @permission_required('auth.jabber_broadcast') def jabber_broadcast_view(request): - logger.debug("jabber_broadcast_view called by user %s" % request.user) + logger.debug(f"jabber_broadcast_view called by user {request.user}") allchoices = [] if request.user.has_perm('auth.jabber_broadcast_all'): allchoices.append(('all', 'all')) @@ -92,7 +92,7 @@ def jabber_broadcast_view(request): if request.method == 'POST': form = JabberBroadcastForm(request.POST) form.fields['group'].choices = allchoices - logger.debug("Received POST request containing form, valid: %s" % form.is_valid()) + logger.debug(f"Received POST request containing form, valid: {form.is_valid()}") if form.is_valid(): main_char = request.user.profile.main_character logger.debug(f"Processing Jabber broadcast for user {request.user} with main character {main_char}") @@ -112,8 +112,8 @@ def jabber_broadcast_view(request): OpenfireManager.send_broadcast_message(group_to_send, message_to_send) - messages.success(request, _('Sent Jabber broadcast to %s' % group_to_send)) - logger.info("Sent Jabber broadcast on behalf of user %s" % request.user) + messages.success(request, _('Sent Jabber broadcast to {}'.format(group_to_send))) + logger.info(f"Sent Jabber broadcast on behalf of user {request.user}") except PingBotException as e: messages.error(request, e) @@ -130,26 +130,26 @@ def jabber_broadcast_view(request): @login_required @permission_required(ACCESS_PERM) def set_jabber_password(request): - logger.debug("set_jabber_password called by user %s" % request.user) + logger.debug(f"set_jabber_password called by user {request.user}") if request.method == 'POST': logger.debug("Received POST request with form.") form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) + logger.debug(f"Form is valid: {form.is_valid()}") if form.is_valid() and OpenfireTasks.has_account(request.user): password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) + logger.debug(f"Form contains password of length {len(password)}") result = OpenfireManager.update_user_pass(request.user.openfire.username, password=password) if result != "": - logger.info("Successfully set Jabber password for user %s" % request.user) + logger.info(f"Successfully set Jabber password for user {request.user}") messages.success(request, _('Set jabber password.')) else: - logger.error("Failed to install custom Jabber password for user %s" % request.user) + logger.error(f"Failed to install custom Jabber password for user {request.user}") messages.error(request, _('An error occurred while processing your Jabber account.')) return redirect("services:services") else: logger.debug("Request is not type POST - providing empty form.") form = ServicePasswordForm() - logger.debug("Rendering form for user %s" % request.user) + logger.debug(f"Rendering form for user {request.user}") context = {'form': form, 'service': 'Jabber'} return render(request, 'services/service_password.html', context=context) diff --git a/allianceauth/services/modules/phpbb3/admin.py b/allianceauth/services/modules/phpbb3/admin.py index 0e97132d..25c1f019 100644 --- a/allianceauth/services/modules/phpbb3/admin.py +++ b/allianceauth/services/modules/phpbb3/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from .models import Phpbb3User + from ...admin import ServicesUserAdmin +from .models import Phpbb3User @admin.register(Phpbb3User) diff --git a/allianceauth/services/modules/phpbb3/auth_hooks.py b/allianceauth/services/modules/phpbb3/auth_hooks.py index 9b45694e..c8081818 100644 --- a/allianceauth/services/modules/phpbb3/auth_hooks.py +++ b/allianceauth/services/modules/phpbb3/auth_hooks.py @@ -5,6 +5,7 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import Phpbb3Tasks from .urls import urlpatterns @@ -39,7 +40,7 @@ class Phpbb3Service(ServicesHook): Phpbb3Tasks.update_groups.delay(user.pk) def update_all_groups(self): - logger.debug('Update all %s groups called' % self.name) + logger.debug(f'Update all {self.name} groups called') Phpbb3Tasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/phpbb3/manager.py b/allianceauth/services/modules/phpbb3/manager.py index 7e6120a6..addac2ee 100644 --- a/allianceauth/services/modules/phpbb3/manager.py +++ b/allianceauth/services/modules/phpbb3/manager.py @@ -1,16 +1,16 @@ -import random -import string import calendar +import logging +import random import re +import string from datetime import datetime from passlib.apps import phpbb3_context -from django.db import connections -from allianceauth.eveonline.models import EveCharacter - -import logging from django.conf import settings +from django.db import connections + +from allianceauth.eveonline.models import EveCharacter logger = logging.getLogger(__name__) @@ -19,39 +19,39 @@ TABLE_PREFIX = getattr(settings, 'PHPBB3_TABLE_PREFIX', 'phpbb_') class Phpbb3Manager: - SQL_ADD_USER = r"INSERT INTO %susers (username, username_clean, " \ + SQL_ADD_USER = rf"INSERT INTO {TABLE_PREFIX}users (username, username_clean, " \ r"user_password, user_email, group_id, user_regdate, user_permissions, " \ - r"user_sig, user_lang) VALUES (%%s, %%s, %%s, %%s, %%s, %%s, %%s, %%s, 'en')" % TABLE_PREFIX + r"user_sig, user_lang) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'en')" - SQL_DEL_USER = r"DELETE FROM %susers where username = %%s" % TABLE_PREFIX + SQL_DEL_USER = rf"DELETE FROM {TABLE_PREFIX}users where username = %s" - SQL_DIS_USER = r"UPDATE %susers SET user_email= %%s, user_password=%%s WHERE username = %%s" % TABLE_PREFIX + SQL_DIS_USER = rf"UPDATE {TABLE_PREFIX}users SET user_email= %s, user_password=%s WHERE username = %s" - SQL_USER_ID_FROM_USERNAME = r"SELECT user_id from %susers WHERE username = %%s" % TABLE_PREFIX + SQL_USER_ID_FROM_USERNAME = rf"SELECT user_id from {TABLE_PREFIX}users WHERE username = %s" - SQL_ADD_USER_GROUP = r"INSERT INTO %suser_group (group_id, user_id, user_pending) VALUES (%%s, %%s, %%s)" % TABLE_PREFIX + SQL_ADD_USER_GROUP = rf"INSERT INTO {TABLE_PREFIX}user_group (group_id, user_id, user_pending) VALUES (%s, %s, %s)" - SQL_GET_GROUP_ID = r"SELECT group_id from %sgroups WHERE group_name = %%s" % TABLE_PREFIX + SQL_GET_GROUP_ID = rf"SELECT group_id from {TABLE_PREFIX}groups WHERE group_name = %s" - SQL_ADD_GROUP = r"INSERT INTO %sgroups (group_name,group_desc,group_legend) VALUES (%%s,%%s,0)" % TABLE_PREFIX + SQL_ADD_GROUP = rf"INSERT INTO {TABLE_PREFIX}groups (group_name,group_desc,group_legend) VALUES (%s,%s,0)" - SQL_UPDATE_USER_PASSWORD = r"UPDATE %susers SET user_password = %%s WHERE username = %%s" % TABLE_PREFIX + SQL_UPDATE_USER_PASSWORD = rf"UPDATE {TABLE_PREFIX}users SET user_password = %s WHERE username = %s" - SQL_REMOVE_USER_GROUP = r"DELETE FROM %suser_group WHERE user_id=%%s AND group_id=%%s " % TABLE_PREFIX + SQL_REMOVE_USER_GROUP = rf"DELETE FROM {TABLE_PREFIX}user_group WHERE user_id=%s AND group_id=%s " - SQL_GET_ALL_GROUPS = r"SELECT group_id, group_name FROM %sgroups" % TABLE_PREFIX + SQL_GET_ALL_GROUPS = rf"SELECT group_id, group_name FROM {TABLE_PREFIX}groups" - SQL_GET_USER_GROUPS = r"SELECT %(prefix)sgroups.group_name FROM %(prefix)sgroups , %(prefix)suser_group WHERE " \ - r"%(prefix)suser_group.group_id = %(prefix)sgroups.group_id AND user_id=%%s" % {'prefix': TABLE_PREFIX} + SQL_GET_USER_GROUPS = rf"SELECT {TABLE_PREFIX}groups.group_name FROM {TABLE_PREFIX}groups , {TABLE_PREFIX}user_group WHERE " \ + rf"{TABLE_PREFIX}user_group.group_id = {TABLE_PREFIX}groups.group_id AND user_id=%s" - SQL_ADD_USER_AVATAR = r"UPDATE %susers SET user_avatar_type=2, user_avatar_width=64, user_avatar_height=64, " \ - "user_avatar=%%s WHERE user_id = %%s" % TABLE_PREFIX + SQL_ADD_USER_AVATAR = rf"UPDATE {TABLE_PREFIX}users SET user_avatar_type=2, user_avatar_width=64, user_avatar_height=64, " \ + "user_avatar=%s WHERE user_id = %s" - SQL_CLEAR_USER_PERMISSIONS = r"UPDATE %susers SET user_permissions = '' WHERE user_id = %%s" % TABLE_PREFIX + SQL_CLEAR_USER_PERMISSIONS = rf"UPDATE {TABLE_PREFIX}users SET user_permissions = '' WHERE user_id = %s" - SQL_DEL_SESSION = r"DELETE FROM %ssessions where session_user_id = %%s" % TABLE_PREFIX + SQL_DEL_SESSION = rf"DELETE FROM {TABLE_PREFIX}sessions where session_user_id = %s" - SQL_DEL_AUTOLOGIN = r"DELETE FROM %ssessions_keys where user_id = %%s" % TABLE_PREFIX + SQL_DEL_AUTOLOGIN = rf"DELETE FROM {TABLE_PREFIX}sessions_keys where user_id = %s" def __init__(self): pass @@ -85,7 +85,7 @@ class Phpbb3Manager: @staticmethod def __get_group_id(groupname): - logger.debug("Getting phpbb3 group id for groupname %s" % groupname) + logger.debug(f"Getting phpbb3 group id for groupname {groupname}") cursor = connections['phpbb3'].cursor() cursor.execute(Phpbb3Manager.SQL_GET_GROUP_ID, [groupname]) row = cursor.fetchone() @@ -94,7 +94,7 @@ class Phpbb3Manager: @staticmethod def __get_user_id(username): - logger.debug("Getting phpbb3 user id for username %s" % username) + logger.debug(f"Getting phpbb3 user id for username {username}") cursor = connections['phpbb3'].cursor() cursor.execute(Phpbb3Manager.SQL_USER_ID_FROM_USERNAME, [username]) row = cursor.fetchone() @@ -102,7 +102,7 @@ class Phpbb3Manager: logger.debug(f"Got phpbb user id {row[0]} for username {username}") return row[0] else: - logger.error("Username %s not found on phpbb. Unable to determine user id." % username) + logger.error(f"Username {username} not found on phpbb. Unable to determine user id.") return None @staticmethod @@ -114,12 +114,12 @@ class Phpbb3Manager: out = {} for row in rows: out[row[1]] = row[0] - logger.debug("Got phpbb groups %s" % out) + logger.debug(f"Got phpbb groups {out}") return out @staticmethod def __get_user_groups(userid): - logger.debug("Getting phpbb3 user id %s groups" % userid) + logger.debug(f"Getting phpbb3 user id {userid} groups") cursor = connections['phpbb3'].cursor() cursor.execute(Phpbb3Manager.SQL_GET_USER_GROUPS, [userid]) out = [row[0] for row in cursor.fetchall()] @@ -134,10 +134,10 @@ class Phpbb3Manager: @staticmethod def __create_group(groupname): - logger.debug("Creating phpbb3 group %s" % groupname) + logger.debug(f"Creating phpbb3 group {groupname}") cursor = connections['phpbb3'].cursor() cursor.execute(Phpbb3Manager.SQL_ADD_GROUP, [groupname, groupname]) - logger.info("Created phpbb group %s" % groupname) + logger.info(f"Created phpbb group {groupname}") return Phpbb3Manager.__get_group_id(groupname) @staticmethod @@ -148,7 +148,7 @@ class Phpbb3Manager: cursor.execute(Phpbb3Manager.SQL_ADD_USER_GROUP, [groupid, userid, 0]) cursor.execute(Phpbb3Manager.SQL_CLEAR_USER_PERMISSIONS, [userid]) logger.info(f"Added phpbb user id {userid} to group id {groupid}") - except: + except Exception: logger.exception(f"Unable to add phpbb user id {userid} to group id {groupid}") pass @@ -160,14 +160,13 @@ class Phpbb3Manager: cursor.execute(Phpbb3Manager.SQL_REMOVE_USER_GROUP, [userid, groupid]) cursor.execute(Phpbb3Manager.SQL_CLEAR_USER_PERMISSIONS, [userid]) logger.info(f"Removed phpbb user id {userid} from group id {groupid}") - except: + except Exception: logger.exception(f"Unable to remove phpbb user id {userid} from group id {groupid}") pass @staticmethod def add_user(username, email, groups, characterid): - logger.debug("Adding phpbb user with username {}, email {}, groups {}, characterid {}".format( - username, email, groups, characterid)) + logger.debug(f"Adding phpbb user with username {username}, email {email}, groups {groups}, characterid {characterid}") cursor = connections['phpbb3'].cursor() username_clean = Phpbb3Manager.__santatize_username(username) @@ -176,7 +175,7 @@ class Phpbb3Manager: logger.debug(f"Proceeding to add phpbb user {username_clean} and pwhash starting with {pwhash[0:5]}") # check if the username was simply revoked if Phpbb3Manager.check_user(username_clean): - logger.warning("Unable to add phpbb user with username %s - already exists. Updating user instead." % username) + logger.warning(f"Unable to add phpbb user with username {username} - already exists. Updating user instead.") Phpbb3Manager.__update_user_info(username_clean, email, pwhash) else: try: @@ -186,16 +185,16 @@ class Phpbb3Manager: "", ""]) Phpbb3Manager.update_groups(username_clean, groups) Phpbb3Manager.__add_avatar(username_clean, characterid) - logger.info("Added phpbb user %s" % username_clean) - except: - logger.exception("Unable to add phpbb user %s" % username_clean) + logger.info(f"Added phpbb user {username_clean}") + except Exception: + logger.exception(f"Unable to add phpbb user {username_clean}") pass return username_clean, password @staticmethod def disable_user(username): - logger.debug("Disabling phpbb user %s" % username) + logger.debug(f"Disabling phpbb user {username}") cursor = connections['phpbb3'].cursor() password = Phpbb3Manager.__gen_hash(Phpbb3Manager.__generate_random_pass()) @@ -207,22 +206,22 @@ class Phpbb3Manager: cursor.execute(Phpbb3Manager.SQL_DEL_AUTOLOGIN, [userid]) cursor.execute(Phpbb3Manager.SQL_DEL_SESSION, [userid]) Phpbb3Manager.update_groups(username, []) - logger.info("Disabled phpbb user %s" % username) + logger.info(f"Disabled phpbb user {username}") return True except TypeError: - logger.exception("TypeError occured while disabling user %s - failed to disable." % username) + logger.exception(f"TypeError occured while disabling user {username} - failed to disable.") return False @staticmethod def delete_user(username): - logger.debug("Deleting phpbb user %s" % username) + logger.debug(f"Deleting phpbb user {username}") cursor = connections['phpbb3'].cursor() if Phpbb3Manager.check_user(username): cursor.execute(Phpbb3Manager.SQL_DEL_USER, [username]) - logger.info("Deleted phpbb user %s" % username) + logger.info(f"Deleted phpbb user {username}") return True - logger.error("Unable to delete phpbb user %s - user not found on phpbb." % username) + logger.error(f"Unable to delete phpbb user {username} - user not found on phpbb.") return False @staticmethod @@ -237,7 +236,7 @@ class Phpbb3Manager: remgroups = user_groups - act_groups logger.info(f"Updating phpbb user {username} groups - adding {addgroups}, removing {remgroups}") for g in addgroups: - if not g in forum_groups: + if g not in forum_groups: forum_groups[g] = Phpbb3Manager.__create_group(g) Phpbb3Manager.__add_user_to_group(userid, forum_groups[g]) @@ -257,27 +256,26 @@ class Phpbb3Manager: try: cursor.execute(Phpbb3Manager.SQL_REMOVE_USER_GROUP, [userid, groupid]) logger.info(f"Removed phpbb user {username} from group {group}") - except: + except Exception: logger.exception( - "Exception prevented removal of phpbb user {} with id {} from group {} with id {}".format( - username, userid, group, groupid)) + f"Exception prevented removal of phpbb user {username} with id {userid} from group {group} with id {groupid}") pass @staticmethod def check_user(username): - logger.debug("Checking phpbb username %s" % username) + logger.debug(f"Checking phpbb username {username}") cursor = connections['phpbb3'].cursor() cursor.execute(Phpbb3Manager.SQL_USER_ID_FROM_USERNAME, [Phpbb3Manager.__santatize_username(username)]) row = cursor.fetchone() if row: - logger.debug("Found user %s on phpbb" % username) + logger.debug(f"Found user {username} on phpbb") return True - logger.debug("User %s not found on phpbb" % username) + logger.debug(f"User {username} not found on phpbb") return False @staticmethod def update_user_password(username, characterid, password=None): - logger.debug("Updating phpbb user %s password" % username) + logger.debug(f"Updating phpbb user {username} password") cursor = connections['phpbb3'].cursor() if not password: password = Phpbb3Manager.__generate_random_pass() @@ -287,9 +285,9 @@ class Phpbb3Manager: f"Proceeding to update phpbb user {username} password with pwhash starting with {pwhash[0:5]}") cursor.execute(Phpbb3Manager.SQL_UPDATE_USER_PASSWORD, [pwhash, username]) Phpbb3Manager.__add_avatar(username, characterid) - logger.info("Updated phpbb user %s password." % username) + logger.info(f"Updated phpbb user {username} password.") return password - logger.error("Unable to update phpbb user %s password - user not found on phpbb." % username) + logger.error(f"Unable to update phpbb user {username} password - user not found on phpbb.") return "" @staticmethod @@ -299,7 +297,7 @@ class Phpbb3Manager: cursor = connections['phpbb3'].cursor() try: cursor.execute(Phpbb3Manager.SQL_DIS_USER, [email, password, username]) - logger.info("Updated phpbb user %s info" % username) - except: - logger.exception("Unable to update phpbb user %s info." % username) + logger.info(f"Updated phpbb user {username} info") + except Exception: + logger.exception(f"Unable to update phpbb user {username} info.") pass diff --git a/allianceauth/services/modules/phpbb3/migrations/0001_initial.py b/allianceauth/services/modules/phpbb3/migrations/0001_initial.py index 151aa380..a822d713 100644 --- a/allianceauth/services/modules/phpbb3/migrations/0001_initial.py +++ b/allianceauth/services/modules/phpbb3/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:28 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/phpbb3/migrations/0002_service_permissions.py b/allianceauth/services/modules/phpbb3/migrations/0002_service_permissions.py index 6757c2df..cb28e494 100644 --- a/allianceauth/services/modules/phpbb3/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/phpbb3/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/phpbb3/tasks.py b/allianceauth/services/modules/phpbb3/tasks.py index 53cf1331..88d9d3bb 100644 --- a/allianceauth/services/modules/phpbb3/tasks.py +++ b/allianceauth/services/modules/phpbb3/tasks.py @@ -1,11 +1,14 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from celery import shared_task -from allianceauth.services.tasks import QueueOnce + from allianceauth.notifications import notify from allianceauth.services.hooks import NameFormatter +from allianceauth.services.tasks import QueueOnce + from .manager import Phpbb3Manager from .models import Phpbb3User @@ -38,7 +41,7 @@ class Phpbb3Tasks: @shared_task(bind=True, name="phpbb3.update_groups", base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating phpbb3 groups for user %s" % user) + logger.debug(f"Updating phpbb3 groups for user {user}") if Phpbb3Tasks.has_account(user): groups = [user.profile.state.name] for group in user.groups.all(): @@ -46,10 +49,10 @@ class Phpbb3Tasks: logger.debug(f"Updating user {user} phpbb3 groups to {groups}") try: Phpbb3Manager.update_groups(user.phpbb3.username, groups) - except: - logger.exception("Phpbb group sync failed for %s, retrying in 10 mins" % user) + except Exception: + logger.exception(f"Phpbb group sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s phpbb3 groups." % user) + logger.debug(f"Updated user {user} phpbb3 groups.") else: logger.debug("User does not have a Phpbb3 account") diff --git a/allianceauth/services/modules/phpbb3/tests.py b/allianceauth/services/modules/phpbb3/tests.py index f6d250bf..ff013eb1 100644 --- a/allianceauth/services/modules/phpbb3/tests.py +++ b/allianceauth/services/modules/phpbb3/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class Phpbb3HooksTestCase(TestCase): member = AuthUtils.create_member(self.member) Phpbb3User.objects.create(user=member, username=self.member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = Phpbb3Service add_permissions() @@ -86,7 +86,8 @@ class Phpbb3HooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - none_phpbb3 = User.objects.get(username=self.none_user).phpbb3 + user = User.objects.get(username=self.none_user) + _ = user.phpbb3 @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') def test_delete_user(self, manager): @@ -98,7 +99,8 @@ class Phpbb3HooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - phpbb3_user = User.objects.get(username=self.member).phpbb3 + user = User.objects.get(username=self.member) + _ = user.phpbb3 def test_render_services_ctrl(self): service = self.service() @@ -158,7 +160,8 @@ class Phpbb3ViewsTestCase(TestCase): self.assertTrue(manager.disable_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - phpbb3_user = User.objects.get(pk=self.member.pk).phpbb3 + user = User.objects.get(pk=self.member.pk) + _ = user.phpbb3 @mock.patch(MODULE_PATH + '.views.Phpbb3Manager') def test_set_password(self, manager): diff --git a/allianceauth/services/modules/phpbb3/views.py b/allianceauth/services/modules/phpbb3/views.py index b847c8af..ba1ceb84 100644 --- a/allianceauth/services/modules/phpbb3/views.py +++ b/allianceauth/services/modules/phpbb3/views.py @@ -2,7 +2,7 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from allianceauth.services.forms import ServicePasswordForm @@ -19,7 +19,7 @@ ACCESS_PERM = 'phpbb3.access_phpbb3' @login_required @permission_required(ACCESS_PERM) def activate_forum(request): - logger.debug("activate_forum called by user %s" % request.user) + logger.debug(f"activate_forum called by user {request.user}") # Valid now we get the main characters character = request.user.profile.main_character logger.debug(f"Adding phpbb user for user {request.user} with main character {character}") @@ -28,9 +28,9 @@ def activate_forum(request): # if empty we failed if result[0] != "": Phpbb3User.objects.update_or_create(user=request.user, defaults={'username': result[0]}) - logger.debug("Updated authserviceinfo for user %s with forum credentials. Updating groups." % request.user) + logger.debug(f"Updated authserviceinfo for user {request.user} with forum credentials. Updating groups.") Phpbb3Tasks.update_groups.delay(request.user.pk) - logger.info("Successfully activated forum for user %s" % request.user) + logger.info(f"Successfully activated forum for user {request.user}") messages.success(request, _('Activated forum account.')) credentials = { 'username': result[0], @@ -38,7 +38,7 @@ def activate_forum(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'Forum'}) else: - logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate forum for user {request.user}") messages.error(request, _('An error occurred while processing your forum account.')) return redirect("services:services") @@ -46,13 +46,13 @@ def activate_forum(request): @login_required @permission_required(ACCESS_PERM) def deactivate_forum(request): - logger.debug("deactivate_forum called by user %s" % request.user) + logger.debug(f"deactivate_forum called by user {request.user}") # false we failed if Phpbb3Tasks.delete_user(request.user): - logger.info("Successfully deactivated forum for user %s" % request.user) + logger.info(f"Successfully deactivated forum for user {request.user}") messages.success(request, _('Deactivated forum account.')) else: - logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate forum for user {request.user}") messages.error(request, _('An error occurred while processing your forum account.')) return redirect("services:services") @@ -60,13 +60,13 @@ def deactivate_forum(request): @login_required @permission_required(ACCESS_PERM) def reset_forum_password(request): - logger.debug("reset_forum_password called by user %s" % request.user) + logger.debug(f"reset_forum_password called by user {request.user}") if Phpbb3Tasks.has_account(request.user): character = request.user.profile.main_character result = Phpbb3Manager.update_user_password(request.user.phpbb3.username, character.character_id) # false we failed if result != "": - logger.info("Successfully reset forum password for user %s" % request.user) + logger.info(f"Successfully reset forum password for user {request.user}") messages.success(request, _('Reset forum password.')) credentials = { 'username': request.user.phpbb3.username, @@ -74,7 +74,7 @@ def reset_forum_password(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'Forum'}) - logger.error("Unsuccessful attempt to reset forum password for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset forum password for user {request.user}") messages.error(request, _('An error occurred while processing your forum account.')) return redirect("services:services") @@ -82,28 +82,28 @@ def reset_forum_password(request): @login_required @permission_required(ACCESS_PERM) def set_forum_password(request): - logger.debug("set_forum_password called by user %s" % request.user) + logger.debug(f"set_forum_password called by user {request.user}") if request.method == 'POST': logger.debug("Received POST request with form.") form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) + logger.debug(f"Form is valid: {form.is_valid()}") if form.is_valid() and Phpbb3Tasks.has_account(request.user): password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) + logger.debug(f"Form contains password of length {len(password)}") character = request.user.profile.main_character result = Phpbb3Manager.update_user_password(request.user.phpbb3.username, character.character_id, password=password) if result != "": - logger.info("Successfully set forum password for user %s" % request.user) + logger.info(f"Successfully set forum password for user {request.user}") messages.success(request, _('Set forum password.')) else: - logger.error("Failed to install custom forum password for user %s" % request.user) + logger.error(f"Failed to install custom forum password for user {request.user}") messages.error(request, _('An error occurred while processing your forum account.')) return redirect("services:services") else: logger.debug("Request is not type POST - providing empty form.") form = ServicePasswordForm() - logger.debug("Rendering form for user %s" % request.user) + logger.debug(f"Rendering form for user {request.user}") context = {'form': form, 'service': 'Forum'} return render(request, 'services/service_password.html', context=context) diff --git a/allianceauth/services/modules/smf/admin.py b/allianceauth/services/modules/smf/admin.py index db2dcae2..dc17f1a4 100644 --- a/allianceauth/services/modules/smf/admin.py +++ b/allianceauth/services/modules/smf/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import SmfUser from ...admin import ServicesUserAdmin +from .models import SmfUser @admin.register(SmfUser) diff --git a/allianceauth/services/modules/smf/auth_hooks.py b/allianceauth/services/modules/smf/auth_hooks.py index 8065e935..99859277 100644 --- a/allianceauth/services/modules/smf/auth_hooks.py +++ b/allianceauth/services/modules/smf/auth_hooks.py @@ -5,6 +5,7 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import SmfTasks from .urls import urlpatterns @@ -45,7 +46,7 @@ class SmfService(ServicesHook): SmfTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing def update_all_groups(self): - logger.debug('Update all %s groups called' % self.name) + logger.debug(f'Update all {self.name} groups called') SmfTasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/smf/manager.py b/allianceauth/services/modules/smf/manager.py index d0f52617..4113bfe1 100644 --- a/allianceauth/services/modules/smf/manager.py +++ b/allianceauth/services/modules/smf/manager.py @@ -1,17 +1,16 @@ -import random -import string import calendar -from datetime import datetime import hashlib import logging +import random import re -from typing import Tuple +import string +from datetime import datetime from packaging import version -from django.db import connections from django.conf import settings from django.contrib.auth.models import User +from django.db import connections from allianceauth.eveonline.models import EveCharacter @@ -26,43 +25,43 @@ class SmfManager: pass # For SMF < 2.1 - SQL_ADD_USER_SMF_20 = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \ + SQL_ADD_USER_SMF_20 = rf"INSERT INTO {TABLE_PREFIX}members (member_name, passwd, email_address, date_registered, real_name," \ r" buddy_list, message_labels, openid_uri, signature, ignore_boards) " \ - r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0, 0, 0)" % TABLE_PREFIX + r"VALUES (%s, %s, %s, %s, %s, 0, 0, 0, 0, 0)" # For SMF >= 2.1 - SQL_ADD_USER_SMF_21 = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \ + SQL_ADD_USER_SMF_21 = rf"INSERT INTO {TABLE_PREFIX}members (member_name, passwd, email_address, date_registered, real_name," \ r" buddy_list, signature, ignore_boards) " \ - r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0)" % TABLE_PREFIX + r"VALUES (%s, %s, %s, %s, %s, 0, 0, 0)" # returns something like »window.smfVersion = "SMF 2.0.19";« - SQL_GET_CURRENT_SMF_VERSION = r"SELECT data FROM %sadmin_info_files WHERE filename = %%s" % TABLE_PREFIX + SQL_GET_CURRENT_SMF_VERSION = rf"SELECT data FROM {TABLE_PREFIX}admin_info_files WHERE filename = %s" - SQL_DEL_USER = r"DELETE FROM %smembers where member_name = %%s" % TABLE_PREFIX + SQL_DEL_USER = rf"DELETE FROM {TABLE_PREFIX}members where member_name = %s" - SQL_UPD_USER = r"UPDATE %smembers SET email_address = %%s, passwd = %%s, real_name = %%s WHERE member_name = %%s" % TABLE_PREFIX + SQL_UPD_USER = rf"UPDATE {TABLE_PREFIX}members SET email_address = %s, passwd = %s, real_name = %s WHERE member_name = %s" - SQL_UPD_DISPLAY_NAME = r"UPDATE %smembers SET real_name = %%s WHERE member_name = %%s" % TABLE_PREFIX + SQL_UPD_DISPLAY_NAME = rf"UPDATE {TABLE_PREFIX}members SET real_name = %s WHERE member_name = %s" - SQL_DIS_USER = r"UPDATE %smembers SET email_address = %%s, passwd = %%s WHERE member_name = %%s" % TABLE_PREFIX + SQL_DIS_USER = rf"UPDATE {TABLE_PREFIX}members SET email_address = %s, passwd = %s WHERE member_name = %s" - SQL_USER_ID_FROM_USERNAME = r"SELECT id_member from %smembers WHERE member_name = %%s" % TABLE_PREFIX + SQL_USER_ID_FROM_USERNAME = rf"SELECT id_member from {TABLE_PREFIX}members WHERE member_name = %s" - SQL_ADD_USER_GROUP = r"UPDATE %smembers SET additional_groups = %%s WHERE id_member = %%s" % TABLE_PREFIX + SQL_ADD_USER_GROUP = rf"UPDATE {TABLE_PREFIX}members SET additional_groups = %s WHERE id_member = %s" - SQL_GET_GROUP_ID = r"SELECT id_group from %smembergroups WHERE group_name = %%s" % TABLE_PREFIX + SQL_GET_GROUP_ID = rf"SELECT id_group from {TABLE_PREFIX}membergroups WHERE group_name = %s" - SQL_ADD_GROUP = r"INSERT INTO %smembergroups (group_name,description) VALUES (%%s,%%s)" % TABLE_PREFIX + SQL_ADD_GROUP = rf"INSERT INTO {TABLE_PREFIX}membergroups (group_name,description) VALUES (%s,%s)" - SQL_UPDATE_USER_PASSWORD = r"UPDATE %smembers SET passwd = %%s WHERE member_name = %%s" % TABLE_PREFIX + SQL_UPDATE_USER_PASSWORD = rf"UPDATE {TABLE_PREFIX}members SET passwd = %s WHERE member_name = %s" - SQL_REMOVE_USER_GROUP = r"UPDATE %smembers SET additional_groups = %%s WHERE id_member = %%s" % TABLE_PREFIX + SQL_REMOVE_USER_GROUP = rf"UPDATE {TABLE_PREFIX}members SET additional_groups = %s WHERE id_member = %s" - SQL_GET_ALL_GROUPS = r"SELECT id_group, group_name FROM %smembergroups" % TABLE_PREFIX + SQL_GET_ALL_GROUPS = rf"SELECT id_group, group_name FROM {TABLE_PREFIX}membergroups" - SQL_GET_USER_GROUPS = r"SELECT additional_groups FROM %smembers WHERE id_member = %%s" % TABLE_PREFIX + SQL_GET_USER_GROUPS = rf"SELECT additional_groups FROM {TABLE_PREFIX}members WHERE id_member = %s" - SQL_ADD_USER_AVATAR = r"UPDATE %smembers SET avatar = %%s WHERE id_member = %%s" % TABLE_PREFIX + SQL_ADD_USER_AVATAR = rf"UPDATE {TABLE_PREFIX}members SET avatar = %s WHERE id_member = %s" @classmethod def _get_current_smf_version(cls) -> str: diff --git a/allianceauth/services/modules/smf/migrations/0001_initial.py b/allianceauth/services/modules/smf/migrations/0001_initial.py index b4ce2f34..a0626fca 100644 --- a/allianceauth/services/modules/smf/migrations/0001_initial.py +++ b/allianceauth/services/modules/smf/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:28 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/smf/migrations/0002_service_permissions.py b/allianceauth/services/modules/smf/migrations/0002_service_permissions.py index 611111eb..780dee84 100644 --- a/allianceauth/services/modules/smf/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/smf/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/smf/migrations/0003_set_smf_displayed_names.py b/allianceauth/services/modules/smf/migrations/0003_set_smf_displayed_names.py index ea4f82a9..ede12672 100644 --- a/allianceauth/services/modules/smf/migrations/0003_set_smf_displayed_names.py +++ b/allianceauth/services/modules/smf/migrations/0003_set_smf_displayed_names.py @@ -1,6 +1,8 @@ from django.db import migrations + from ..manager import SmfManager + def on_migrate(apps, schema_editor): SmfUser = apps.get_model("smf", "SmfUser") db_alias = schema_editor.connection.alias @@ -9,7 +11,7 @@ def on_migrate(apps, schema_editor): for smf_user in all_smf_users: try: auth_user = smf_user.user - except: + except Exception: pass else: SmfManager.update_display_name(auth_user) diff --git a/allianceauth/services/modules/smf/models.py b/allianceauth/services/modules/smf/models.py index 4e44821d..f8cd257e 100644 --- a/allianceauth/services/modules/smf/models.py +++ b/allianceauth/services/modules/smf/models.py @@ -8,10 +8,10 @@ class SmfUser(models.Model): related_name='smf') username = models.CharField(max_length=254) - def __str__(self): - return self.username - class Meta: permissions = ( ("access_smf", "Can access the SMF service"), ) + + def __str__(self) -> str: + return self.username diff --git a/allianceauth/services/modules/smf/tasks.py b/allianceauth/services/modules/smf/tasks.py index 3f6392e0..9cac03a3 100644 --- a/allianceauth/services/modules/smf/tasks.py +++ b/allianceauth/services/modules/smf/tasks.py @@ -1,11 +1,14 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from celery import shared_task -from allianceauth.services.tasks import QueueOnce + from allianceauth.notifications import notify from allianceauth.services.hooks import NameFormatter +from allianceauth.services.tasks import QueueOnce + from .manager import SmfManager from .models import SmfUser @@ -42,7 +45,7 @@ class SmfTasks: @shared_task(bind=True, name="smf.update_groups", base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating smf groups for user %s" % user) + logger.debug(f"Updating smf groups for user {user}") if SmfTasks.has_account(user): groups = [user.profile.state.name] for group in user.groups.all(): @@ -50,10 +53,10 @@ class SmfTasks: logger.debug(f"Updating user {user} smf groups to {groups}") try: SmfManager.update_groups(user.smf.username, groups) - except: - logger.exception("smf group sync failed for %s, retrying in 10 mins" % user) + except Exception: + logger.exception(f"smf group sync failed for {user}, retrying in 10 mins") raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s smf groups." % user) + logger.debug(f"Updated user {user} smf groups.") else: logger.debug("User does not have an smf account") @@ -74,7 +77,7 @@ class SmfTasks: f"SMF displayed name sync failed for {user}, " "user does not have a SMF account" ) - except: + except Exception: logger.exception( f"SMF displayed name sync failed for {user}, retrying in 10 mins" ) diff --git a/allianceauth/services/modules/smf/tests.py b/allianceauth/services/modules/smf/tests.py index b6c20895..c308959c 100644 --- a/allianceauth/services/modules/smf/tests.py +++ b/allianceauth/services/modules/smf/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class SmfHooksTestCase(TestCase): member = AuthUtils.create_member(self.member) SmfUser.objects.create(user=member, username=self.member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = SmfService add_permissions() @@ -86,7 +86,8 @@ class SmfHooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - none_smf = User.objects.get(username=self.none_user).smf + user = User.objects.get(username=self.none_user) + _ = user.smf @mock.patch(MODULE_PATH + '.tasks.SmfManager') def test_delete_user(self, manager): @@ -98,7 +99,8 @@ class SmfHooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - smf_user = User.objects.get(username=self.member).smf + user = User.objects.get(username=self.member) + _ = user.smf def test_render_services_ctrl(self): service = self.service() @@ -158,7 +160,8 @@ class SmfViewsTestCase(TestCase): self.assertTrue(manager.disable_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - smf_user = User.objects.get(pk=self.member.pk).smf + user =User.objects.get(pk=self.member.pk) + _ = user.smf @mock.patch(MODULE_PATH + '.views.SmfManager') def test_set_password(self, manager): diff --git a/allianceauth/services/modules/smf/views.py b/allianceauth/services/modules/smf/views.py index 7302b936..71460b71 100644 --- a/allianceauth/services/modules/smf/views.py +++ b/allianceauth/services/modules/smf/views.py @@ -2,7 +2,7 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from allianceauth.services.forms import ServicePasswordForm @@ -70,14 +70,14 @@ def activate_smf(request): @login_required @permission_required(ACCESS_PERM) def deactivate_smf(request): - logger.debug("deactivate_smf called by user %s" % request.user) + logger.debug(f"deactivate_smf called by user {request.user}") result = SmfTasks.delete_user(request.user) # false we failed if result: - logger.info("Successfully deactivated SMF for user %s" % request.user) + logger.info(f"Successfully deactivated SMF for user {request.user}") messages.success(request, _('Deactivated SMF account.')) else: - logger.error("Unsuccessful attempt to activate SMF for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate SMF for user {request.user}") messages.error(request, _('An error occurred while processing your SMF account.')) return redirect("services:services") @@ -85,20 +85,20 @@ def deactivate_smf(request): @login_required @permission_required(ACCESS_PERM) def reset_smf_password(request): - logger.debug("reset_smf_password called by user %s" % request.user) + logger.debug(f"reset_smf_password called by user {request.user}") character = request.user.profile.main_character if SmfTasks.has_account(request.user) and character is not None: result = SmfManager.update_user_password(request.user.smf.username, character.character_id) # false we failed if result != "": - logger.info("Successfully reset SMF password for user %s" % request.user) + logger.info(f"Successfully reset SMF password for user {request.user}") messages.success(request, _('Reset SMF password.')) credentials = { 'username': request.user.smf.username, 'password': result, } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'SMF'}) - logger.error("Unsuccessful attempt to reset SMF password for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset SMF password for user {request.user}") messages.error(request, _('An error occurred while processing your SMF account.')) return redirect("services:services") @@ -106,27 +106,27 @@ def reset_smf_password(request): @login_required @permission_required(ACCESS_PERM) def set_smf_password(request): - logger.debug("set_smf_password called by user %s" % request.user) + logger.debug(f"set_smf_password called by user {request.user}") if request.method == 'POST': logger.debug("Received POST request with form.") form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) + logger.debug(f"Form is valid: {form.is_valid()}") character = request.user.profile.main_character if form.is_valid() and SmfTasks.has_account(request.user) and character is not None: password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) + logger.debug(f"Form contains password of length {len(password)}") result = SmfManager.update_user_password(request.user.smf.username, character.character_id, password=password) if result != "": - logger.info("Successfully set SMF password for user %s" % request.user) + logger.info(f"Successfully set SMF password for user {request.user}") messages.success(request, _('Set SMF password.')) else: - logger.error("Failed to install custom SMF password for user %s" % request.user) + logger.error(f"Failed to install custom SMF password for user {request.user}") messages.error(request, _('An error occurred while processing your SMF account.')) return redirect("services:services") else: logger.debug("Request is not type POST - providing empty form.") form = ServicePasswordForm() - logger.debug("Rendering form for user %s" % request.user) + logger.debug(f"Rendering form for user {request.user}") context = {'form': form, 'service': 'SMF'} return render(request, 'services/service_password.html', context=context) diff --git a/allianceauth/services/modules/teamspeak3/admin.py b/allianceauth/services/modules/teamspeak3/admin.py index 92a1fcfe..c00a7604 100644 --- a/allianceauth/services/modules/teamspeak3/admin.py +++ b/allianceauth/services/modules/teamspeak3/admin.py @@ -1,9 +1,11 @@ from django.contrib import admin from django.contrib.auth.models import Group -from .models import AuthTS, Teamspeak3User, StateGroup, TSgroup -from ...admin import ServicesUserAdmin + from allianceauth.groupmanagement.models import ReservedGroupName +from ...admin import ServicesUserAdmin +from .models import AuthTS, StateGroup, Teamspeak3User, TSgroup + @admin.register(Teamspeak3User) class Teamspeak3UserAdmin(ServicesUserAdmin): @@ -40,7 +42,7 @@ class AuthTSgroupAdmin(admin.ModelAdmin): description='ts groups' ) def _ts_group(self, obj): - return [x for x in obj.ts_group.all().order_by('ts_group_id')] + return list(obj.ts_group.all().order_by('ts_group_id')) # _ts_group.admin_order_field = 'profile__state' diff --git a/allianceauth/services/modules/teamspeak3/apps.py b/allianceauth/services/modules/teamspeak3/apps.py index f46e3c92..693dec3a 100644 --- a/allianceauth/services/modules/teamspeak3/apps.py +++ b/allianceauth/services/modules/teamspeak3/apps.py @@ -6,4 +6,4 @@ class Teamspeak3ServiceConfig(AppConfig): label = 'teamspeak3' def ready(self): - from . import signals + pass diff --git a/allianceauth/services/modules/teamspeak3/auth_hooks.py b/allianceauth/services/modules/teamspeak3/auth_hooks.py index 20e82326..5c818d86 100644 --- a/allianceauth/services/modules/teamspeak3/auth_hooks.py +++ b/allianceauth/services/modules/teamspeak3/auth_hooks.py @@ -1,10 +1,11 @@ import logging -from django.template.loader import render_to_string from django.conf import settings +from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import Teamspeak3Tasks from .urls import urlpatterns @@ -34,7 +35,7 @@ class Teamspeak3Service(ServicesHook): self.delete_user(user, notify_user=True) def update_all_groups(self): - logger.debug('Update all %s groups called' % self.name) + logger.debug(f'Update all {self.name} groups called') Teamspeak3Tasks.update_all_groups.delay() def service_active_for_user(self, user): diff --git a/allianceauth/services/modules/teamspeak3/manager.py b/allianceauth/services/modules/teamspeak3/manager.py index 519a8a47..9d5c7b22 100644 --- a/allianceauth/services/modules/teamspeak3/manager.py +++ b/allianceauth/services/modules/teamspeak3/manager.py @@ -2,10 +2,11 @@ import logging from django.conf import settings -from .util.ts3 import TS3Server, TeamspeakError -from .models import TSgroup from allianceauth.groupmanagement.models import ReservedGroupName +from .models import TSgroup +from .util.ts3 import TeamspeakError, TS3Server + logger = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class Teamspeak3Manager: return sanatized def _get_userid(self, uid): - logger.debug("Looking for uid %s on TS3 server." % uid) + logger.debug(f"Looking for uid {uid} on TS3 server.") try: ret = self.server.send_command('customsearch', {'ident': 'sso_uid', 'pattern': uid}) if ret and 'keys' in ret and 'cldbid' in ret['keys']: @@ -64,21 +65,21 @@ class Teamspeak3Manager: return None def _group_id_by_name(self, groupname): - logger.debug("Looking for group %s on TS3 server." % groupname) + logger.debug(f"Looking for group {groupname} on TS3 server.") group_cache = self.server.send_command('servergrouplist') - logger.debug("Received group cache from server: %s" % group_cache) + logger.debug(f"Received group cache from server: {group_cache}") for group in group_cache: if group['keys']['type'] != '1': continue - logger.debug("Checking group %s" % group) + logger.debug(f"Checking group {group}") if group['keys']['name'] == groupname: logger.debug("Found group {}, returning id {}".format(groupname, group['keys']['sgid'])) return group['keys']['sgid'] - logger.debug("Group %s not found on server." % groupname) + logger.debug(f"Group {groupname} not found on server.") return None def _create_group(self, groupname): - logger.debug("Creating group %s on TS3 server." % groupname) + logger.debug(f"Creating group {groupname} on TS3 server.") sgid = self._group_id_by_name(groupname) if not sgid: logger.debug("Group does not yet exist. Proceeding with creation.") @@ -98,8 +99,8 @@ class Teamspeak3Manager: return sgid def _user_group_list(self, cldbid): - logger.debug("Retrieving group list for user with id %s" % cldbid) - server = Teamspeak3Manager.__get_created_server() + logger.debug(f"Retrieving group list for user with id {cldbid}") + Teamspeak3Manager.__get_created_server() try: groups = self.server.send_command('servergroupsbyclientid', {'cldbid': cldbid}) except TeamspeakError as e: @@ -107,23 +108,23 @@ class Teamspeak3Manager: groups = [] else: raise e - logger.debug("Retrieved group list: %s" % groups) + logger.debug(f"Retrieved group list: {groups}") outlist = {} - if type(groups) == list: + if type(groups) is list: logger.debug("Recieved multiple groups. Iterating.") for group in groups: outlist[group['keys']['name']] = group['keys']['sgid'] - elif type(groups) == dict: + elif type(groups) is dict: logger.debug("Recieved single group.") outlist[groups['keys']['name']] = groups['keys']['sgid'] - logger.debug("Returning name/id pairing: %s" % outlist) + logger.debug(f"Returning name/id pairing: {outlist}") return outlist def _group_list(self): logger.debug("Retrieving group list on TS3 server.") group_cache = self.server.send_command('servergrouplist') - logger.debug("Received group cache from server: %s" % group_cache) + logger.debug(f"Received group cache from server: {group_cache}") outlist = {} if group_cache: for group in group_cache: @@ -133,7 +134,7 @@ class Teamspeak3Manager: outlist[group['keys']['name']] = group['keys']['sgid'] else: logger.error("Received empty group cache while retrieving group cache from TS3 server. 1024 error.") - logger.debug("Returning name/id pairing: %s" % outlist) + logger.debug(f"Returning name/id pairing: {outlist}") return outlist def _add_user_to_group(self, uid, groupid): @@ -175,11 +176,11 @@ class Teamspeak3Manager: except TeamspeakError as e: logger.error(f"Error occurred while syncing TS group db: {str(e)}") except Exception: - logger.exception(f"An unhandled exception has occurred while syncing TS groups.") + logger.exception("An unhandled exception has occurred while syncing TS groups.") def add_user(self, user, fmt_name): username_clean = self.__santatize_username(fmt_name[:30]) - logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean) + logger.debug(f"Adding user to TS3 server with cleaned username {username_clean}") server_groups = self._group_list() state = user.profile.state.name @@ -191,16 +192,16 @@ class Teamspeak3Manager: try: ret = self.server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': state_group_id, 'tokenid2': 0, 'tokendescription': username_clean, - 'tokencustomset': "ident=sso_uid value=%s" % username_clean}) + 'tokencustomset': f"ident=sso_uid value={username_clean}"}) except TeamspeakError as e: logger.error(f"Failed to add teamspeak user {username_clean}: {str(e)}") return "","" try: token = ret['keys']['token'] - logger.info("Created permission token for user %s on TS3 server" % username_clean) + logger.info(f"Created permission token for user {username_clean} on TS3 server") return username_clean, token - except: + except Exception: logger.exception(f"Failed to add teamspeak user {username_clean} - received response: {ret}") return "", "" @@ -216,10 +217,10 @@ class Teamspeak3Manager: for client in clients: try: if client['keys']['client_database_id'] == user: - logger.debug("Found user %s on TS3 server - issuing deletion command." % user) + logger.debug(f"Found user {user} on TS3 server - issuing deletion command.") self.server.send_command('clientkick', {'clid': client['keys']['clid'], 'reasonid': 5, 'reasonmsg': 'Auth service deleted'}) - except: + except Exception: logger.exception(f"Failed to delete user id {uid} from TS3 - received response {client}") return False try: @@ -228,13 +229,13 @@ class Teamspeak3Manager: logger.error(f"Failed to delete teamspeak user {uid}: {str(e)}") return False if ret == '0': - logger.info("Deleted user with id %s from TS3 server." % uid) + logger.info(f"Deleted user with id {uid} from TS3 server.") return True else: logger.exception(f"Failed to delete user id {uid} from TS3 - received response {ret}") return False else: - logger.warning("User with id %s not found on TS3 server. Assuming succesful deletion." % uid) + logger.warning(f"User with id {uid} not found on TS3 server. Assuming succesful deletion.") return True def check_user_exists(self, uid): @@ -244,7 +245,7 @@ class Teamspeak3Manager: return False def generate_new_permissionkey(self, uid, user, username): - logger.debug("Re-issuing permission key for user id %s" % uid) + logger.debug(f"Re-issuing permission key for user id {uid}") self.delete_user(uid) return self.add_user(user, username) @@ -255,11 +256,11 @@ class Teamspeak3Manager: remgroups = [] if userid is not None: user_ts_groups = self._user_group_list(userid) - logger.debug("User has groups on TS3 server: %s" % user_ts_groups) + logger.debug(f"User has groups on TS3 server: {user_ts_groups}") for key in user_ts_groups: user_ts_groups[key] = int(user_ts_groups[key]) for ts_group_key in ts_groups: - logger.debug("Checking if user has group %s on TS3 server." % ts_group_key) + logger.debug(f"Checking if user has group {ts_group_key} on TS3 server.") if ts_groups[ts_group_key] not in user_ts_groups.values(): addgroups.append(ts_groups[ts_group_key]) for user_ts_group_key in user_ts_groups: diff --git a/allianceauth/services/modules/teamspeak3/migrations/0001_initial.py b/allianceauth/services/modules/teamspeak3/migrations/0001_initial.py index 3f22e872..d2211c4c 100644 --- a/allianceauth/services/modules/teamspeak3/migrations/0001_initial.py +++ b/allianceauth/services/modules/teamspeak3/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 01:11 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/teamspeak3/migrations/0003_teamspeak3user.py b/allianceauth/services/modules/teamspeak3/migrations/0003_teamspeak3user.py index dbe71b6c..382f8d13 100644 --- a/allianceauth/services/modules/teamspeak3/migrations/0003_teamspeak3user.py +++ b/allianceauth/services/modules/teamspeak3/migrations/0003_teamspeak3user.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:14 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/teamspeak3/migrations/0004_service_permissions.py b/allianceauth/services/modules/teamspeak3/migrations/0004_service_permissions.py index 4023cbff..779efad8 100644 --- a/allianceauth/services/modules/teamspeak3/migrations/0004_service_permissions.py +++ b/allianceauth/services/modules/teamspeak3/migrations/0004_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/teamspeak3/migrations/0005_stategroup.py b/allianceauth/services/modules/teamspeak3/migrations/0005_stategroup.py index 0cd994eb..6c043f68 100644 --- a/allianceauth/services/modules/teamspeak3/migrations/0005_stategroup.py +++ b/allianceauth/services/modules/teamspeak3/migrations/0005_stategroup.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.10 on 2018-02-23 06:13 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/teamspeak3/models.py b/allianceauth/services/modules/teamspeak3/models.py index 5569a35c..2cba2e06 100644 --- a/allianceauth/services/modules/teamspeak3/models.py +++ b/allianceauth/services/modules/teamspeak3/models.py @@ -1,5 +1,6 @@ +from django.contrib.auth.models import Group, User from django.db import models -from django.contrib.auth.models import User, Group + from allianceauth.authentication.models import State @@ -11,14 +12,13 @@ class Teamspeak3User(models.Model): uid = models.CharField(max_length=254) perm_key = models.CharField(max_length=254) - def __str__(self): - return self.uid - class Meta: permissions = ( ("access_teamspeak3", "Can access the Teamspeak3 service"), ) + def __str__(self): + return self.uid class TSgroup(models.Model): ts_group_id = models.IntegerField(primary_key=True) @@ -56,3 +56,6 @@ class UserTSgroup(models.Model): class StateGroup(models.Model): state = models.ForeignKey(State, on_delete=models.CASCADE) ts_group = models.ForeignKey(TSgroup, on_delete=models.CASCADE) + + def __str__(self): + return self.pk diff --git a/allianceauth/services/modules/teamspeak3/signals.py b/allianceauth/services/modules/teamspeak3/signals.py index 59054d10..bef0329d 100644 --- a/allianceauth/services/modules/teamspeak3/signals.py +++ b/allianceauth/services/modules/teamspeak3/signals.py @@ -1,13 +1,13 @@ import logging from django.db import transaction -from django.db.models.signals import m2m_changed -from django.db.models.signals import post_delete -from django.db.models.signals import post_save +from django.db.models.signals import m2m_changed, post_delete, post_save from django.dispatch import receiver + from allianceauth.authentication.signals import state_changed -from .tasks import Teamspeak3Tasks + from .models import AuthTS, StateGroup +from .tasks import Teamspeak3Tasks logger = logging.getLogger(__name__) @@ -26,13 +26,13 @@ def m2m_changed_authts_group(sender, instance, action, *args, **kwargs): @receiver(post_save, sender=AuthTS) def post_save_authts(sender, instance, *args, **kwargs): - logger.debug("Received post_save from %s" % instance) + logger.debug(f"Received post_save from {instance}") transaction.on_commit(trigger_all_ts_update) @receiver(post_delete, sender=AuthTS) def post_delete_authts(sender, instance, *args, **kwargs): - logger.debug("Received post_delete signal from %s" % instance) + logger.debug(f"Received post_delete signal from {instance}") transaction.on_commit(trigger_all_ts_update) diff --git a/allianceauth/services/modules/teamspeak3/tasks.py b/allianceauth/services/modules/teamspeak3/tasks.py index c65e123d..61b0792f 100644 --- a/allianceauth/services/modules/teamspeak3/tasks.py +++ b/allianceauth/services/modules/teamspeak3/tasks.py @@ -1,13 +1,16 @@ import logging +from celery import shared_task + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from celery import shared_task -from allianceauth.services.tasks import QueueOnce + from allianceauth.notifications import notify from allianceauth.services.hooks import NameFormatter +from allianceauth.services.tasks import QueueOnce + from .manager import Teamspeak3Manager -from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User +from .models import AuthTS, Teamspeak3User, TSgroup, UserTSgroup from .util.ts3 import TeamspeakError logger = logging.getLogger(__name__) @@ -59,7 +62,7 @@ class Teamspeak3Tasks: @shared_task(bind=True, name="teamspeak3.update_groups", base=QueueOnce) def update_groups(self, pk): user = User.objects.get(pk=pk) - logger.debug("Updating user %s teamspeak3 groups" % user) + logger.debug(f"Updating user {user} teamspeak3 groups") if Teamspeak3Tasks.has_account(user): usergroups = user.groups.all() groups = {} @@ -75,7 +78,7 @@ class Teamspeak3Tasks: try: with Teamspeak3Manager() as ts3man: ts3man.update_groups(user.teamspeak3.uid, groups) - logger.debug("Updated user %s teamspeak3 groups." % user) + logger.debug(f"Updated user {user} teamspeak3 groups.") except TeamspeakError as e: logger.error(f"Error occured while syncing TS groups for {user}: {str(e)}") raise self.retry(countdown=60*10) diff --git a/allianceauth/services/modules/teamspeak3/tests.py b/allianceauth/services/modules/teamspeak3/tests.py index 62f9ad85..d6deb2ca 100644 --- a/allianceauth/services/modules/teamspeak3/tests.py +++ b/allianceauth/services/modules/teamspeak3/tests.py @@ -1,22 +1,22 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.admin import AdminSite +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist from django.db.models import signals -from django.contrib.admin import AdminSite +from django.test import RequestFactory, TestCase -from allianceauth.tests.auth_utils import AuthUtils -from .auth_hooks import Teamspeak3Service -from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup -from .tasks import Teamspeak3Tasks -from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts -from .admin import AuthTSgroupAdmin - -from .manager import Teamspeak3Manager -from .util.ts3 import TeamspeakError from allianceauth.groupmanagement.models import ReservedGroupName +from allianceauth.tests.auth_utils import AuthUtils + +from .admin import AuthTSgroupAdmin +from .auth_hooks import Teamspeak3Service +from .manager import Teamspeak3Manager +from .models import AuthTS, StateGroup, Teamspeak3User, TSgroup +from .signals import m2m_changed_authts_group, post_delete_authts, post_save_authts +from .tasks import Teamspeak3Tasks +from .util.ts3 import TeamspeakError MODULE_PATH = 'allianceauth.services.modules.teamspeak3' DEFAULT_AUTH_GROUP = 'Member' @@ -31,7 +31,7 @@ def add_permissions(): class Teamspeak3HooksTestCase(TestCase): def setUp(self): # Inert signals before setup begins - with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update'): self.member = 'member_user' member = AuthUtils.create_member(self.member) Teamspeak3User.objects.create(user=member, uid=self.member, perm_key='123ABC') @@ -104,7 +104,7 @@ class Teamspeak3HooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called) with self.assertRaises(ObjectDoesNotExist): - none_teamspeak3 = User.objects.get(username=self.none_user).teamspeak3 + _ = User.objects.get(username=self.none_user).teamspeak3 @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') def test_delete_user(self, manager): @@ -116,7 +116,8 @@ class Teamspeak3HooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called) with self.assertRaises(ObjectDoesNotExist): - teamspeak3_user = User.objects.get(username=self.member).teamspeak3 + _ = User.objects.get(username=self.member) + def test_render_services_ctrl(self): service = self.service() @@ -140,7 +141,7 @@ class Teamspeak3HooksTestCase(TestCase): class Teamspeak3ViewsTestCase(TestCase): def setUp(self): # Inert signals before setup begins - with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update'): self.member = AuthUtils.create_member('auth_member') self.member.email = 'auth_member@example.com' self.member.save() @@ -177,7 +178,6 @@ class Teamspeak3ViewsTestCase(TestCase): @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') def test_verify_submit(self, manager, forms_manager): self.login() - expected_username = 'auth_member' forms_instance = manager.return_value.__enter__.return_value forms_instance._get_userid.return_value = '1234' @@ -200,7 +200,8 @@ class Teamspeak3ViewsTestCase(TestCase): self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - teamspeak3_user = User.objects.get(pk=self.member.pk).teamspeak3 + user = User.objects.get(pk=self.member.pk) + _ = user.teamspeak3 @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') @@ -242,7 +243,7 @@ class Teamspeak3SignalsTestCase(TestCase): self.member = AuthUtils.create_member('auth_member') # Suppress signals action while setting up - with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update'): ts_member_group = TSgroup.objects.create(ts_group_id=1, ts_group_name='Member') self.m2m_member = AuthTS.objects.create(auth_group=Group.objects.get(name='Member')) self.m2m_member.ts_group.add(ts_member_group) diff --git a/allianceauth/services/modules/teamspeak3/util/ts3.py b/allianceauth/services/modules/teamspeak3/util/ts3.py index 81ae74a6..358a1b58 100644 --- a/allianceauth/services/modules/teamspeak3/util/ts3.py +++ b/allianceauth/services/modules/teamspeak3/util/ts3.py @@ -1,5 +1,5 @@ -import telnetlib import logging +import telnetlib class ConnectionError: @@ -40,7 +40,7 @@ class TS3Proto: try: self._conn = telnetlib.Telnet(host=ip, port=port, timeout=5) self._connected = True - except: + except Exception: # raise ConnectionError(ip, port) raise @@ -55,7 +55,7 @@ class TS3Proto: try: self.send("quit") self._conn.close() - except: + except Exception: self._log.exception('Error while disconnecting') self._connected = False self._log.info('Disconnected') @@ -69,7 +69,7 @@ class TS3Proto: self._conn.read_very_eager() # Send command - self.send('%s\n' % cmd) + self.send(f'{cmd}\n') data = [] @@ -130,7 +130,7 @@ class TS3Proto: # Add in options if opts: for opt in opts: - cstr.append("-%s" % opt) + cstr.append(f"-{opt}") return " ".join(cstr) @@ -205,7 +205,7 @@ class TS3Proto: def send(self, payload): if self._connected: - self._log.debug('Sent: %s' % payload) + self._log.debug(f'Sent: {payload}') self._conn.write(payload.encode('utf-8')) diff --git a/allianceauth/services/modules/teamspeak3/views.py b/allianceauth/services/modules/teamspeak3/views.py index be6d7cd3..1b4bd833 100644 --- a/allianceauth/services/modules/teamspeak3/views.py +++ b/allianceauth/services/modules/teamspeak3/views.py @@ -1,14 +1,14 @@ import logging +from django.conf import settings from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect -from django.conf import settings +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ -from .manager import Teamspeak3Manager from .forms import TeamspeakJoinForm +from .manager import Teamspeak3Manager from .models import Teamspeak3User from .tasks import Teamspeak3Tasks @@ -20,7 +20,7 @@ ACCESS_PERM = 'teamspeak3.access_teamspeak3' @login_required @permission_required(ACCESS_PERM) def activate_teamspeak3(request): - logger.debug("activate_teamspeak3 called by user %s" % request.user) + logger.debug(f"activate_teamspeak3 called by user {request.user}") character = request.user.profile.main_character with Teamspeak3Manager() as ts3man: @@ -30,11 +30,11 @@ def activate_teamspeak3(request): # if its empty we failed if result[0] != "": Teamspeak3User.objects.update_or_create(user=request.user, defaults={'uid': result[0], 'perm_key': result[1]}) - logger.debug("Updated authserviceinfo for user %s with TeamSpeak3 credentials. Updating groups." % request.user) - logger.info("Successfully activated TeamSpeak3 for user %s" % request.user) + logger.debug(f"Updated authserviceinfo for user {request.user} with TeamSpeak3 credentials. Updating groups.") + logger.info(f"Successfully activated TeamSpeak3 for user {request.user}") messages.success(request, _('Activated TeamSpeak3 account.')) return redirect("teamspeak3:verify") - logger.error("Unsuccessful attempt to activate TeamSpeak3 for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate TeamSpeak3 for user {request.user}") messages.error(request, _('An error occurred while processing your TeamSpeak3 account.')) return redirect("services:services") @@ -42,15 +42,15 @@ def activate_teamspeak3(request): @login_required @permission_required(ACCESS_PERM) def verify_teamspeak3(request): - logger.debug("verify_teamspeak3 called by user %s" % request.user) + logger.debug(f"verify_teamspeak3 called by user {request.user}") if not Teamspeak3Tasks.has_account(request.user): - logger.warning("Unable to validate user %s teamspeak: no teamspeak data" % request.user) + logger.warning(f"Unable to validate user {request.user} teamspeak: no teamspeak data") return redirect("services:services") if request.method == "POST": form = TeamspeakJoinForm(request.POST) if form.is_valid(): Teamspeak3Tasks.update_groups.delay(request.user.pk) - logger.debug("Validated user %s joined TS server" % request.user) + logger.debug(f"Validated user {request.user} joined TS server") return redirect("services:services") else: form = TeamspeakJoinForm(initial={'username': request.user.teamspeak3.uid}) @@ -65,12 +65,12 @@ def verify_teamspeak3(request): @login_required @permission_required(ACCESS_PERM) def deactivate_teamspeak3(request): - logger.debug("deactivate_teamspeak3 called by user %s" % request.user) + logger.debug(f"deactivate_teamspeak3 called by user {request.user}") if Teamspeak3Tasks.has_account(request.user) and Teamspeak3Tasks.delete_user(request.user): - logger.info("Successfully deactivated TeamSpeak3 for user %s" % request.user) + logger.info(f"Successfully deactivated TeamSpeak3 for user {request.user}") messages.success(request, _('Deactivated TeamSpeak3 account.')) else: - logger.error("Unsuccessful attempt to deactivate TeamSpeak3 for user %s" % request.user) + logger.error(f"Unsuccessful attempt to deactivate TeamSpeak3 for user {request.user}") messages.error(request, _('An error occurred while processing your TeamSpeak3 account.')) return redirect("services:services") @@ -78,25 +78,25 @@ def deactivate_teamspeak3(request): @login_required @permission_required(ACCESS_PERM) def reset_teamspeak3_perm(request): - logger.debug("reset_teamspeak3_perm called by user %s" % request.user) + logger.debug(f"reset_teamspeak3_perm called by user {request.user}") if not Teamspeak3Tasks.has_account(request.user): return redirect("services:services") - logger.debug("Deleting TeamSpeak3 user for user %s" % request.user) + logger.debug(f"Deleting TeamSpeak3 user for user {request.user}") with Teamspeak3Manager() as ts3man: ts3man.delete_user(request.user.teamspeak3.uid) - logger.debug("Generating new permission key for user %s" % request.user) + logger.debug(f"Generating new permission key for user {request.user}") result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, request.user, Teamspeak3Tasks.get_username(request.user)) # if blank we failed if result[0] != "": Teamspeak3User.objects.update_or_create(user=request.user, defaults={'uid': result[0], 'perm_key': result[1]}) - logger.debug("Updated authserviceinfo for user %s with TeamSpeak3 credentials. Updating groups." % request.user) + logger.debug(f"Updated authserviceinfo for user {request.user} with TeamSpeak3 credentials. Updating groups.") Teamspeak3Tasks.update_groups.delay(request.user.pk) - logger.info("Successfully reset TeamSpeak3 permission key for user %s" % request.user) + logger.info(f"Successfully reset TeamSpeak3 permission key for user {request.user}") messages.success(request, _('Reset TeamSpeak3 permission key.')) else: - logger.error("Unsuccessful attempt to reset TeamSpeak3 permission key for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset TeamSpeak3 permission key for user {request.user}") messages.error(request, _('An error occurred while processing your TeamSpeak3 account.')) return redirect("services:services") diff --git a/allianceauth/services/modules/xenforo/admin.py b/allianceauth/services/modules/xenforo/admin.py index 20ccbd7e..cdd134dd 100644 --- a/allianceauth/services/modules/xenforo/admin.py +++ b/allianceauth/services/modules/xenforo/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import XenforoUser from ...admin import ServicesUserAdmin +from .models import XenforoUser @admin.register(XenforoUser) diff --git a/allianceauth/services/modules/xenforo/auth_hooks.py b/allianceauth/services/modules/xenforo/auth_hooks.py index 0faa23ac..add23b08 100644 --- a/allianceauth/services/modules/xenforo/auth_hooks.py +++ b/allianceauth/services/modules/xenforo/auth_hooks.py @@ -4,6 +4,7 @@ from django.template.loader import render_to_string from allianceauth import hooks from allianceauth.services.hooks import ServicesHook + from .tasks import XenforoTasks from .urls import urlpatterns diff --git a/allianceauth/services/modules/xenforo/manager.py b/allianceauth/services/modules/xenforo/manager.py index 82e7018f..5c58d493 100644 --- a/allianceauth/services/modules/xenforo/manager.py +++ b/allianceauth/services/modules/xenforo/manager.py @@ -1,12 +1,12 @@ +import json +import logging import random import string + import requests -import json from django.conf import settings -import logging - logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/xenforo/migrations/0001_initial.py b/allianceauth/services/modules/xenforo/migrations/0001_initial.py index 7bb8dce7..a86d12f4 100644 --- a/allianceauth/services/modules/xenforo/migrations/0001_initial.py +++ b/allianceauth/services/modules/xenforo/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.2 on 2016-12-12 03:14 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/services/modules/xenforo/migrations/0002_service_permissions.py b/allianceauth/services/modules/xenforo/migrations/0002_service_permissions.py index 4bc03a4a..7bee799b 100644 --- a/allianceauth/services/modules/xenforo/migrations/0002_service_permissions.py +++ b/allianceauth/services/modules/xenforo/migrations/0002_service_permissions.py @@ -1,12 +1,12 @@ # Generated by Django 1.10.5 on 2017-02-02 05:59 -from django.db import migrations -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.management import create_permissions - import logging +from django.conf import settings +from django.contrib.auth.management import create_permissions +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + logger = logging.getLogger(__name__) diff --git a/allianceauth/services/modules/xenforo/models.py b/allianceauth/services/modules/xenforo/models.py index fb326d25..8b7ce337 100644 --- a/allianceauth/services/modules/xenforo/models.py +++ b/allianceauth/services/modules/xenforo/models.py @@ -8,10 +8,11 @@ class XenforoUser(models.Model): related_name='xenforo') username = models.CharField(max_length=254) - def __str__(self): - return self.username class Meta: permissions = ( ("access_xenforo", "Can access the XenForo service"), ) + + def __str__(self): + return self.username diff --git a/allianceauth/services/modules/xenforo/tasks.py b/allianceauth/services/modules/xenforo/tasks.py index 78c7748d..5e349f21 100644 --- a/allianceauth/services/modules/xenforo/tasks.py +++ b/allianceauth/services/modules/xenforo/tasks.py @@ -4,6 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist from allianceauth.notifications import notify from allianceauth.services.hooks import NameFormatter + from .manager import XenForoManager from .models import XenforoUser diff --git a/allianceauth/services/modules/xenforo/tests.py b/allianceauth/services/modules/xenforo/tests.py index 5b68f46d..023af261 100644 --- a/allianceauth/services/modules/xenforo/tests.py +++ b/allianceauth/services/modules/xenforo/tests.py @@ -1,9 +1,9 @@ from unittest import mock -from django.test import TestCase, RequestFactory from django import urls -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist +from django.test import RequestFactory, TestCase from allianceauth.tests.auth_utils import AuthUtils @@ -27,7 +27,7 @@ class XenforoHooksTestCase(TestCase): member = AuthUtils.create_member(self.member) XenforoUser.objects.create(user=member, username=self.member) self.none_user = 'none_user' - none_user = AuthUtils.create_user(self.none_user) + AuthUtils.create_user(self.none_user) self.service = XenforoService add_permissions() @@ -61,7 +61,7 @@ class XenforoHooksTestCase(TestCase): service.validate_user(none_user) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - none_xenforo = User.objects.get(username=self.none_user).xenforo + _ = User.objects.get(username=self.none_user).xenforo @mock.patch(MODULE_PATH + '.tasks.XenForoManager') def test_delete_user(self, manager): @@ -74,7 +74,7 @@ class XenforoHooksTestCase(TestCase): self.assertTrue(result) self.assertTrue(manager.disable_user.called) with self.assertRaises(ObjectDoesNotExist): - xenforo_user = User.objects.get(username=self.member).xenforo + _ = User.objects.get(username=self.member).xenforo def test_render_services_ctrl(self): service = self.service() @@ -139,7 +139,8 @@ class XenforoViewsTestCase(TestCase): self.assertTrue(manager.disable_user.called) self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200) with self.assertRaises(ObjectDoesNotExist): - xenforo_user = User.objects.get(pk=self.member.pk).xenforo + user = User.objects.get(username=self.member.pk) + _ = user.xenoforo @mock.patch(MODULE_PATH + '.views.XenForoManager') def test_set_password(self, manager): diff --git a/allianceauth/services/modules/xenforo/views.py b/allianceauth/services/modules/xenforo/views.py index ac121108..1286e401 100644 --- a/allianceauth/services/modules/xenforo/views.py +++ b/allianceauth/services/modules/xenforo/views.py @@ -2,7 +2,7 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from allianceauth.services.forms import ServicePasswordForm @@ -19,14 +19,14 @@ ACCESS_PERM = 'xenforo.access_xenforo' @login_required @permission_required(ACCESS_PERM) def activate_xenforo_forum(request): - logger.debug("activate_xenforo_forum called by user %s" % request.user) + logger.debug(f"activate_xenforo_forum called by user {request.user}") character = request.user.profile.main_character logger.debug(f"Adding XenForo user for user {request.user} with main character {character}") result = XenForoManager.add_user(XenforoTasks.get_username(request.user), request.user.email) # Based on XenAPI's response codes if result['response']['status_code'] == 200: XenforoUser.objects.update_or_create(user=request.user, defaults={'username': result['username']}) - logger.info("Updated user %s with XenForo credentials. Updating groups." % request.user) + logger.info(f"Updated user {request.user} with XenForo credentials. Updating groups.") messages.success(request, _('Activated XenForo account.')) credentials = { 'username': result['username'], @@ -36,7 +36,7 @@ def activate_xenforo_forum(request): context={'credentials': credentials, 'service': 'XenForo'}) else: - logger.error("Unsuccessful attempt to activate XenForo for user %s" % request.user) + logger.error(f"Unsuccessful attempt to activate XenForo for user {request.user}") messages.error(request, _('An error occurred while processing your XenForo account.')) return redirect("services:services") @@ -44,9 +44,9 @@ def activate_xenforo_forum(request): @login_required @permission_required(ACCESS_PERM) def deactivate_xenforo_forum(request): - logger.debug("deactivate_xenforo_forum called by user %s" % request.user) + logger.debug(f"deactivate_xenforo_forum called by user {request.user}") if XenforoTasks.delete_user(request.user): - logger.info("Successfully deactivated XenForo for user %s" % request.user) + logger.info(f"Successfully deactivated XenForo for user {request.user}") messages.success(request, _('Deactivated XenForo account.')) else: messages.error(request, _('An error occurred while processing your XenForo account.')) @@ -56,12 +56,12 @@ def deactivate_xenforo_forum(request): @login_required @permission_required(ACCESS_PERM) def reset_xenforo_password(request): - logger.debug("reset_xenforo_password called by user %s" % request.user) + logger.debug(f"reset_xenforo_password called by user {request.user}") if XenforoTasks.has_account(request.user): result = XenForoManager.reset_password(request.user.xenforo.username) # Based on XenAPI's response codes if result['response']['status_code'] == 200: - logger.info("Successfully reset XenForo password for user %s" % request.user) + logger.info(f"Successfully reset XenForo password for user {request.user}") messages.success(request, _('Reset XenForo account password.')) credentials = { 'username': request.user.xenforo.username, @@ -69,7 +69,7 @@ def reset_xenforo_password(request): } return render(request, 'services/service_credentials.html', context={'credentials': credentials, 'service': 'XenForo'}) - logger.error("Unsuccessful attempt to reset XenForo password for user %s" % request.user) + logger.error(f"Unsuccessful attempt to reset XenForo password for user {request.user}") messages.error(request, _('An error occurred while processing your XenForo account.')) return redirect("services:services") @@ -77,26 +77,26 @@ def reset_xenforo_password(request): @login_required @permission_required(ACCESS_PERM) def set_xenforo_password(request): - logger.debug("set_xenforo_password called by user %s" % request.user) + logger.debug(f"set_xenforo_password called by user {request.user}") if request.method == 'POST': logger.debug("Received POST request with form.") form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) + logger.debug(f"Form is valid: {form.is_valid()}") if form.is_valid() and XenforoTasks.has_account(request.user): password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) + logger.debug(f"Form contains password of length {len(password)}") result = XenForoManager.update_user_password(request.user.xenforo.username, password) if result['response']['status_code'] == 200: - logger.info("Successfully reset XenForo password for user %s" % request.user) + logger.info(f"Successfully reset XenForo password for user {request.user}") messages.success(request, _('Changed XenForo password.')) else: - logger.error("Failed to install custom XenForo password for user %s" % request.user) + logger.error(f"Failed to install custom XenForo password for user {request.user}") messages.error(request, _('An error occurred while processing your XenForo account.')) return redirect('services:services') else: logger.debug("Request is not type POST - providing empty form.") form = ServicePasswordForm() - logger.debug("Rendering form for user %s" % request.user) + logger.debug(f"Rendering form for user {request.user}") context = {'form': form, 'service': 'Forum'} return render(request, 'services/service_password.html', context=context) diff --git a/allianceauth/services/signals.py b/allianceauth/services/signals.py index 773f8ab6..167aadd8 100644 --- a/allianceauth/services/signals.py +++ b/allianceauth/services/signals.py @@ -1,20 +1,19 @@ import logging from functools import partial -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.core.exceptions import ObjectDoesNotExist from django.db import transaction -from django.db.models.signals import m2m_changed -from django.db.models.signals import pre_delete -from django.db.models.signals import pre_save +from django.db.models.signals import m2m_changed, pre_delete, pre_save from django.dispatch import receiver -from .hooks import ServicesHook -from .tasks import disable_user, update_groups_for_user from allianceauth.authentication.models import State, UserProfile from allianceauth.authentication.signals import state_changed from allianceauth.eveonline.models import EveCharacter +from .hooks import ServicesHook +from .tasks import disable_user, update_groups_for_user + logger = logging.getLogger(__name__) @@ -46,7 +45,7 @@ def m2m_changed_user_groups(sender, instance, action, *args, **kwargs): @receiver(m2m_changed, sender=User.user_permissions.through) def m2m_changed_user_permissions(sender, instance, action, *args, **kwargs): logger.debug(f"Received m2m_changed from user {instance} permissions with action {action}") - logger.debug('sender: %s' % sender) + logger.debug(f'sender: {sender}') if instance.pk and (action == "post_remove" or action == "post_clear"): logger.debug(f"Permissions changed for user {instance}, re-validating services") # Checking permissions for a single user is quite fast, so we don't need to validate @@ -57,7 +56,7 @@ def m2m_changed_user_permissions(sender, instance, action, *args, **kwargs): for svc in ServicesHook.get_services(): try: svc.validate_user(instance) - except: + except Exception: logger.exception( f'Exception running validate_user for services module {svc} on user {instance}') @@ -138,13 +137,13 @@ def check_service_accounts_state_changed(sender, user, state, **kwargs): @receiver(pre_delete, sender=User) def pre_delete_user(sender, instance, *args, **kwargs): - logger.debug("Received pre_delete from %s" % instance) + logger.debug(f"Received pre_delete from {instance}") disable_user(instance) @receiver(pre_save, sender=User) def disable_services_on_inactive(sender, instance, *args, **kwargs): - logger.debug("Received pre_save from %s" % instance) + logger.debug(f"Received pre_save from {instance}") # check if user is being marked active/inactive if not instance.pk: # new model being created @@ -152,7 +151,7 @@ def disable_services_on_inactive(sender, instance, *args, **kwargs): try: old_instance = User.objects.get(pk=instance.pk) if old_instance.is_active and not instance.is_active: - logger.info("Disabling services for inactivation of user %s" % instance) + logger.info(f"Disabling services for inactivation of user {instance}") disable_user(instance) except User.DoesNotExist: pass @@ -183,7 +182,7 @@ def process_main_character_change(sender, instance, *args, **kwargs): try: svc.validate_user(instance.user) svc.sync_nickname(instance.user) - except: + except Exception: logger.exception( "Exception running sync_nickname for services module %s " "on user %s", @@ -213,7 +212,7 @@ def process_main_character_update(sender, instance, *args, **kwargs): try: svc.validate_user(instance.userprofile.user) svc.sync_nickname(instance.userprofile.user) - except: + except Exception: logger.exception(f'Exception running sync_nickname for services module {svc} on user {instance}') except ObjectDoesNotExist: # not a main char ignore diff --git a/allianceauth/services/tasks.py b/allianceauth/services/tasks.py index 58947577..ac0c8c2f 100644 --- a/allianceauth/services/tasks.py +++ b/allianceauth/services/tasks.py @@ -1,11 +1,12 @@ import logging from celery import shared_task +from celery_once import AlreadyQueued, QueueOnce as BaseTask + from django.contrib.auth.models import User -from .hooks import ServicesHook -from celery_once import QueueOnce as BaseTask, AlreadyQueued from django.core.cache import cache +from .hooks import ServicesHook logger = logging.getLogger(__name__) @@ -38,12 +39,12 @@ def validate_services(self, pk): for svc in ServicesHook.get_services(): try: svc.validate_user(user) - except: + except Exception: logger.exception(f'Exception running validate_user for services module {svc} on user {user}') def disable_user(user): - logger.debug('Disabling all services for user %s' % user) + logger.debug(f'Disabling all services for user {user}') for svc in ServicesHook.get_services(): if svc.service_active_for_user(user): svc.delete_user(user) diff --git a/allianceauth/services/tests/test_hooks.py b/allianceauth/services/tests/test_hooks.py index dd3d787b..ba3d125d 100644 --- a/allianceauth/services/tests/test_hooks.py +++ b/allianceauth/services/tests/test_hooks.py @@ -1,7 +1,7 @@ from unittest import TestCase -from allianceauth.services.hooks import UrlHook from allianceauth.groupmanagement import urls +from allianceauth.services.hooks import UrlHook class TestUrlHook(TestCase): diff --git a/allianceauth/services/tests/test_models.py b/allianceauth/services/tests/test_models.py index f03f58c6..05488a6e 100644 --- a/allianceauth/services/tests/test_models.py +++ b/allianceauth/services/tests/test_models.py @@ -1,4 +1,5 @@ from django.test import TestCase + from allianceauth.tests.auth_utils import AuthUtils from ..models import NameFormatConfig diff --git a/allianceauth/services/tests/test_nameformatter.py b/allianceauth/services/tests/test_nameformatter.py index 2b448bc7..e221cf40 100644 --- a/allianceauth/services/tests/test_nameformatter.py +++ b/allianceauth/services/tests/test_nameformatter.py @@ -1,8 +1,10 @@ from django.test import TestCase + +from allianceauth.eveonline.models import EveAllianceInfo, EveCharacter, EveCorporationInfo from allianceauth.tests.auth_utils import AuthUtils -from allianceauth.eveonline.models import EveAllianceInfo, EveCorporationInfo, EveCharacter -from ..models import NameFormatConfig + from ..hooks import NameFormatter +from ..models import NameFormatConfig from ..modules.example.auth_hooks import ExampleService diff --git a/allianceauth/services/tests/test_signals.py b/allianceauth/services/tests/test_signals.py index e8cfdc57..50c36d9c 100644 --- a/allianceauth/services/tests/test_signals.py +++ b/allianceauth/services/tests/test_signals.py @@ -1,14 +1,13 @@ from copy import deepcopy from unittest import mock -from django.test import override_settings, TestCase, TransactionTestCase from django.contrib.auth.models import Group, Permission +from django.test import TestCase, TransactionTestCase, override_settings from allianceauth.authentication.models import State from allianceauth.eveonline.models import EveCharacter from allianceauth.tests.auth_utils import AuthUtils - MODULE_PATH = 'allianceauth.services.signals' diff --git a/allianceauth/services/tests/test_tasks.py b/allianceauth/services/tests/test_tasks.py index 06257a1f..2d8eaf07 100644 --- a/allianceauth/services/tests/test_tasks.py +++ b/allianceauth/services/tests/test_tasks.py @@ -3,10 +3,10 @@ from unittest import mock from celery_once import AlreadyQueued from django.core.cache import cache -from django.test import override_settings, TestCase +from django.test import TestCase, override_settings +from allianceauth.services.tasks import update_groups_for_user, validate_services from allianceauth.tests.auth_utils import AuthUtils -from allianceauth.services.tasks import validate_services, update_groups_for_user from ..tasks import DjangoBackend @@ -55,7 +55,7 @@ class TestDjangoBackend(TestCase): def setUp(self) -> None: cache.delete(self.TEST_KEY) - self.backend = DjangoBackend(dict()) + self.backend = DjangoBackend({}) def test_can_get_lock(self): """ diff --git a/allianceauth/services/urls.py b/allianceauth/services/urls.py index 9c8a8141..72a52e12 100644 --- a/allianceauth/services/urls.py +++ b/allianceauth/services/urls.py @@ -1,6 +1,6 @@ -from django.urls import include +from django.urls import include, path + from allianceauth.hooks import get_hooks -from django.urls import path from . import views diff --git a/allianceauth/services/views.py b/allianceauth/services/views.py index b99604a3..8f8a14ff 100644 --- a/allianceauth/services/views.py +++ b/allianceauth/services/views.py @@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import render from allianceauth.hooks import get_hooks + from .forms import FleetFormatterForm logger = logging.getLogger(__name__) @@ -11,11 +12,11 @@ logger = logging.getLogger(__name__) @login_required def fleet_formatter_view(request): - logger.debug("fleet_formatter_view called by user %s" % request.user) + logger.debug(f"fleet_formatter_view called by user {request.user}") generated = "" if request.method == 'POST': form = FleetFormatterForm(request.POST) - logger.debug("Received POST request containing form, valid: %s" % form.is_valid()) + logger.debug(f"Received POST request containing form, valid: {form.is_valid()}") if form.is_valid(): generated = "Fleet Name: " + form.cleaned_data['fleet_name'] + "\n" generated = generated + "FC: " + form.cleaned_data['fleet_commander'] + "\n" @@ -29,10 +30,10 @@ def fleet_formatter_view(request): generated = generated + "Important: " + form.cleaned_data['important'] + "\n" if form.cleaned_data['comments'] != "": generated = generated + "Why: " + form.cleaned_data['comments'] + "\n" - logger.info("Formatted fleet broadcast for user %s" % request.user) + logger.info(f"Formatted fleet broadcast for user {request.user}") else: form = FleetFormatterForm() - logger.debug("Returning empty form to user %s" % request.user) + logger.debug(f"Returning empty form to user {request.user}") context = {'form': form, 'generated': generated} @@ -41,8 +42,7 @@ def fleet_formatter_view(request): @login_required def services_view(request): - logger.debug("services_view called by user %s" % request.user) - char = request.user.profile.main_character + logger.debug(f"services_view called by user {request.user}") context = {'service_ctrls': []} for fn in get_hooks('services_hook'): # Render hooked services controls diff --git a/allianceauth/srp/admin.py b/allianceauth/srp/admin.py index 9a6685b8..b40a6761 100644 --- a/allianceauth/srp/admin.py +++ b/allianceauth/srp/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin -from allianceauth.srp.models import SrpFleetMain -from allianceauth.srp.models import SrpUserRequest +from allianceauth.srp.models import SrpFleetMain, SrpUserRequest admin.site.register(SrpFleetMain) admin.site.register(SrpUserRequest) diff --git a/allianceauth/srp/managers.py b/allianceauth/srp/managers.py index b37d26b2..cc7cb059 100644 --- a/allianceauth/srp/managers.py +++ b/allianceauth/srp/managers.py @@ -22,7 +22,7 @@ class SRPManager: @staticmethod def get_kill_data(kill_id): - url = ("https://zkillboard.com/api/killID/%s/" % kill_id) + url = (f"https://zkillboard.com/api/killID/{kill_id}/") headers = { 'User-Agent': NAME, 'Content-Type': 'application/json', diff --git a/allianceauth/srp/migrations/0001_initial.py b/allianceauth/srp/migrations/0001_initial.py index db51307b..ba66d1c9 100644 --- a/allianceauth/srp/migrations/0001_initial.py +++ b/allianceauth/srp/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:40 -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/srp/migrations/0004_on_delete.py b/allianceauth/srp/migrations/0004_on_delete.py index 3dbd2083..dd32f3cb 100644 --- a/allianceauth/srp/migrations/0004_on_delete.py +++ b/allianceauth/srp/migrations/0004_on_delete.py @@ -1,7 +1,7 @@ # Generated by Django 1.11.5 on 2017-09-28 02:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/allianceauth/srp/models.py b/allianceauth/srp/models.py index bb4f8280..58369e89 100644 --- a/allianceauth/srp/models.py +++ b/allianceauth/srp/models.py @@ -13,6 +13,11 @@ class SrpFleetMain(models.Model): fleet_commander = models.ForeignKey(EveCharacter, null=True, on_delete=models.SET_NULL) fleet_srp_aar_link = models.CharField(max_length=254, default="") + + + class Meta: + permissions = (('access_srp', 'Can access SRP module'),) + def __str__(self): return self.fleet_name @@ -23,11 +28,6 @@ class SrpFleetMain(models.Model): @property def pending_requests(self): return self.srpuserrequest_set.filter(srp_status='Pending').count() - - class Meta: - permissions = (('access_srp', 'Can access SRP module'),) - - class SrpUserRequest(models.Model): SRP_STATUS_CHOICES = ( ('Pending', 'Pending'), diff --git a/allianceauth/srp/providers.py b/allianceauth/srp/providers.py index f7bf26e3..ac57ec1e 100644 --- a/allianceauth/srp/providers.py +++ b/allianceauth/srp/providers.py @@ -1,4 +1,5 @@ import os + from esi.clients import EsiClientProvider from allianceauth import __version__ diff --git a/allianceauth/srp/tests/test_managers.py b/allianceauth/srp/tests/test_managers.py index 50eac10b..417f762f 100644 --- a/allianceauth/srp/tests/test_managers.py +++ b/allianceauth/srp/tests/test_managers.py @@ -4,13 +4,13 @@ import os from unittest.mock import patch from django.contrib.auth.models import User -from django.utils.timezone import now from django.test import TestCase +from django.utils.timezone import now from allianceauth.tests.auth_utils import AuthUtils from ..managers import SRPManager -from ..models import SrpUserRequest, SrpFleetMain +from ..models import SrpFleetMain, SrpUserRequest MODULE_PATH = 'allianceauth.srp.managers' @@ -22,7 +22,7 @@ currentdir = os.path.dirname(os.path.abspath(inspect.getfile( def load_data(filename): """loads given JSON file from `testdata` sub folder and returns content""" with open( - currentdir + '/testdata/%s.json' % filename, encoding='utf-8' + currentdir + f'/testdata/{filename}.json', encoding='utf-8' ) as f: data = json.load(f) diff --git a/allianceauth/srp/views.py b/allianceauth/srp/views.py index 99f795e9..d8568c4c 100644 --- a/allianceauth/srp/views.py +++ b/allianceauth/srp/views.py @@ -2,23 +2,21 @@ import logging import uuid from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.humanize.templatetags.humanize import intcomma -from django.http import JsonResponse, Http404 -from django.shortcuts import render, redirect, get_object_or_404 +from django.db.models import Sum +from django.http import Http404, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from django.db.models import Sum + from allianceauth.authentication.decorators import permissions_required -from allianceauth.srp.providers import esi from allianceauth.notifications import notify -from .form import SrpFleetMainForm -from .form import SrpFleetMainUpdateForm -from .form import SrpFleetUserRequestForm -from .models import SrpFleetMain -from .models import SrpUserRequest +from allianceauth.srp.providers import esi + +from .form import SrpFleetMainForm, SrpFleetMainUpdateForm, SrpFleetUserRequestForm from .managers import SRPManager +from .models import SrpFleetMain, SrpUserRequest logger = logging.getLogger(__name__) @@ -34,7 +32,7 @@ def random_string(string_length=10): @login_required @permission_required('srp.access_srp') def srp_management(request, all=False): - logger.debug("srp_management called by user %s" % request.user) + logger.debug(f"srp_management called by user {request.user}") fleets = SrpFleetMain.objects.select_related('fleet_commander').prefetch_related('srpuserrequest_set').all() if not all: fleets = fleets.filter(fleet_srp_status="") @@ -44,14 +42,15 @@ def srp_management(request, all=False): context = {"srpfleets": fleets, "totalcost": totalcost} return render(request, 'srp/management.html', context=context) + @login_required @permission_required('srp.access_srp') def srp_fleet_view(request, fleet_id): logger.debug(f"srp_fleet_view called by user {request.user} for fleet id {fleet_id}") try: fleet_main = SrpFleetMain.objects.get(id=fleet_id) - except SrpFleetMain.DoesNotExist: - raise Http404 + except SrpFleetMain.DoesNotExist as e: + raise Http404 from e context = {"fleet_id": fleet_id, "fleet_status": fleet_main.fleet_srp_status, "srpfleetrequests": fleet_main.srpuserrequest_set.select_related('character'), "totalcost": fleet_main.total_cost} @@ -62,13 +61,13 @@ def srp_fleet_view(request, fleet_id): @login_required @permissions_required(('auth.srp_management', 'srp.add_srpfleetmain')) def srp_fleet_add_view(request): - logger.debug("srp_fleet_add_view called by user %s" % request.user) + logger.debug(f"srp_fleet_add_view called by user {request.user}") completed = False completed_srp_code = "" if request.method == 'POST': form = SrpFleetMainForm(request.POST) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): srp_fleet_main = SrpFleetMain() srp_fleet_main.fleet_name = form.cleaned_data['fleet_name'] @@ -167,7 +166,7 @@ def srp_request_view(request, fleet_srp): if request.method == 'POST': form = SrpFleetUserRequestForm(request.POST) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): request_killboard_link = form.cleaned_data['killboard_link'] @@ -193,8 +192,7 @@ def srp_request_view(request, fleet_srp): srp_kill_link = SRPManager.get_kill_id(srp_request.killboard_link) (ship_type_id, ship_value, victim_id) = SRPManager.get_kill_data(srp_kill_link) except ValueError: - logger.debug("User {} Submitted Invalid Killmail Link {} or server could not be reached".format( - request.user, srp_request.killboard_link)) + logger.debug(f"User {request.user} Submitted Invalid Killmail Link {srp_request.killboard_link} or server could not be reached") # THIS SHOULD BE IN FORM VALIDATION messages.error(request, _("Your SRP request Killmail link is invalid. Please make sure you are using zKillboard.")) @@ -206,8 +204,7 @@ def srp_request_view(request, fleet_srp): srp_request.kb_total_loss = ship_value srp_request.post_time = post_time srp_request.save() - logger.info("Created SRP Request on behalf of user {} for fleet name {}".format( - request.user, srp_fleet_main.fleet_name)) + logger.info(f"Created SRP Request on behalf of user {request.user} for fleet name {srp_fleet_main.fleet_name}") messages.success(request, _('Submitted SRP request for your %(ship)s.') % {"ship": srp_request.srp_ship_name}) return redirect("srp:management") @@ -243,7 +240,7 @@ def srp_request_remove(request): srpuserrequest.delete() logger.info(f"Deleted SRP request id {srp_request_id} for user {request.user}") if stored_fleet_view is None: - logger.error("Unable to delete srp requests for user %s - request matching id not found." % (request.user)) + logger.error(f"Unable to delete srp requests for user {request.user} - request matching id not found.") messages.error(request, _('Unable to locate selected SRP request.')) return redirect("srp:management") else: @@ -270,18 +267,15 @@ def srp_request_approve(request): if srpuserrequest.srp_total_amount == 0: srpuserrequest.srp_total_amount = srpuserrequest.kb_total_loss srpuserrequest.save() - logger.info("Approved SRP request id {} for character {} by user {}".format( - srp_request_id, srpuserrequest.character, request.user)) + logger.info(f"Approved SRP request id {srp_request_id} for character {srpuserrequest.character} by user {request.user}") notify( srpuserrequest.character.character_ownership.user, 'SRP Request Approved', level='success', - message='Your SRP request for a {} lost during {} has been approved for {} ISK.'.format( - srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name, - intcomma(srpuserrequest.srp_total_amount)) + message=f'Your SRP request for a {srpuserrequest.srp_ship_name} lost during {srpuserrequest.srp_fleet_main.fleet_name} has been approved for {intcomma(srpuserrequest.srp_total_amount)} ISK.' ) if stored_fleet_view is None: - logger.error("Unable to approve srp request on behalf of user %s - request matching id not found." % (request.user)) + logger.error(f"Unable to approve srp request on behalf of user {request.user} - request matching id not found.") messages.error(request, _('Unable to locate selected SRP request.')) return redirect("srp:management") else: @@ -306,17 +300,15 @@ def srp_request_reject(request): stored_fleet_view = srpuserrequest.srp_fleet_main.id srpuserrequest.srp_status = "Rejected" srpuserrequest.save() - logger.info("SRP request id {} for character {} rejected by {}".format( - srp_request_id, srpuserrequest.character, request.user)) + logger.info(f"SRP request id {srp_request_id} for character {srpuserrequest.character} rejected by {request.user}") notify( srpuserrequest.character.character_ownership.user, 'SRP Request Rejected', level='danger', - message='Your SRP request for a {} lost during {} has been rejected.'.format( - srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name) + message=f'Your SRP request for a {srpuserrequest.srp_ship_name} lost during {srpuserrequest.srp_fleet_main.fleet_name} has been rejected.' ) if stored_fleet_view is None: - logger.error("Unable to reject SRP request on behalf of user %s - request matching id not found." % (request.user)) + logger.error(f"Unable to reject SRP request on behalf of user {request.user} - request matching id not found.") messages.error(request, _('Unable to locate selected SRP request')) return redirect("srp:management") else: @@ -327,8 +319,7 @@ def srp_request_reject(request): @login_required @permission_required('auth.srp_management') def srp_request_update_amount(request, fleet_srp_request_id): - logger.debug("srp_request_update_amount called by user {} for fleet srp request id {}".format( - request.user, fleet_srp_request_id)) + logger.debug(f"srp_request_update_amount called by user {request.user} for fleet srp request id {fleet_srp_request_id}") if SrpUserRequest.objects.filter(id=fleet_srp_request_id).exists() is False: logger.error(f"Unable to locate SRP request id {fleet_srp_request_id} for user {request.user}") @@ -351,7 +342,7 @@ def srp_fleet_edit_view(request, fleet_id): srpfleetmain = get_object_or_404(SrpFleetMain, id=fleet_id) if request.method == 'POST': form = SrpFleetMainUpdateForm(request.POST) - logger.debug("Request type POST contains form valid: %s" % form.is_valid()) + logger.debug(f"Request type POST contains form valid: {form.is_valid()}") if form.is_valid(): srpfleetmain.fleet_srp_aar_link = form.cleaned_data['fleet_aar_link'] srpfleetmain.save() diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index f9f8347b..d7084dc5 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -44,7 +44,7 @@ def decimal_widthratio(this_value, max_value, max_width) -> str: @register.inclusion_tag('allianceauth/admin-status/overview.html') def status_overview() -> dict: response = { - "notifications": list(), + "notifications": [], "current_version": __version__, "tasks_succeeded": 0, "tasks_retried": 0, @@ -144,8 +144,8 @@ def _latests_versions(tags: list) -> tuple: Non-compliant tags will be ignored """ - versions = list() - betas = list() + versions = [] + betas = [] for tag in tags: try: version = Pep440Version(tag.get('name')) @@ -167,7 +167,7 @@ def _latests_versions(tags: list) -> tuple: def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: """returns a list from the GitLab API. Supports paging""" - result = list() + result = [] for page in range(1, max_pages + 1): try: diff --git a/allianceauth/tests/auth_utils.py b/allianceauth/tests/auth_utils.py index 06bfb314..bec16bcb 100644 --- a/allianceauth/tests/auth_utils.py +++ b/allianceauth/tests/auth_utils.py @@ -1,31 +1,29 @@ -from typing import List -from django.contrib.auth.models import User, Group, Permission -from django.db.models.signals import m2m_changed, pre_save, post_save +from django.contrib.auth.models import Group, Permission, User +from django.db.models.signals import m2m_changed, post_save, pre_save from django.test import TestCase from esi.models import Token -from allianceauth.authentication.models import ( - UserProfile, State, get_guest_state -) -from allianceauth.eveonline.models import EveCharacter +from allianceauth.authentication.models import State, UserProfile, get_guest_state from allianceauth.authentication.signals import ( + assign_state_on_active_change, + check_state_on_character_update, + reassess_on_profile_save, state_member_alliances_changed, state_member_characters_changed, state_member_corporations_changed, state_saved, - reassess_on_profile_save, - assign_state_on_active_change, - check_state_on_character_update ) +from allianceauth.eveonline.models import EveCharacter from allianceauth.services.signals import ( + disable_services_on_inactive, m2m_changed_group_permissions, - m2m_changed_user_permissions, m2m_changed_state_permissions, - m2m_changed_user_groups, disable_services_on_inactive, + m2m_changed_user_groups, + m2m_changed_user_permissions, process_main_character_change, - process_main_character_update + process_main_character_update, ) diff --git a/allianceauth/tests/test_auth_utils.py b/allianceauth/tests/test_auth_utils.py index 2fa5215c..62a06bd7 100644 --- a/allianceauth/tests/test_auth_utils.py +++ b/allianceauth/tests/test_auth_utils.py @@ -1,19 +1,14 @@ -from unittest import mock -from django.contrib.auth.models import User, Group, Permission +from django.contrib.auth.models import Group, Permission, User from django.test import TestCase -from allianceauth.eveonline.models import ( - EveCorporationInfo, EveAllianceInfo, EveCharacter -) - from .auth_utils import AuthUtils class TestAuthUtils(TestCase): def test_can_create_user(self): - user = AuthUtils.create_user('Bruce Wayne') + AuthUtils.create_user('Bruce Wayne') self.assertTrue(User.objects.filter(username='Bruce Wayne').exists()) def test_can_add_main_character_2(self): diff --git a/allianceauth/tests/test_urls.py b/allianceauth/tests/test_urls.py index 95315b8d..621ce0ed 100644 --- a/allianceauth/tests/test_urls.py +++ b/allianceauth/tests/test_urls.py @@ -1,5 +1,6 @@ from unittest import TestCase -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + from django.urls import URLPattern from allianceauth.services.hooks import UrlHook diff --git a/allianceauth/theme/bootstrap/auth_hooks.py b/allianceauth/theme/bootstrap/auth_hooks.py index 72fe265a..0ac2de3a 100644 --- a/allianceauth/theme/bootstrap/auth_hooks.py +++ b/allianceauth/theme/bootstrap/auth_hooks.py @@ -1,7 +1,6 @@ from allianceauth import hooks from allianceauth.theme.hooks import ThemeHook - CSS_STATICS = [{ "url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "integrity": "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==" diff --git a/allianceauth/theme/templatetags/theme_tags.py b/allianceauth/theme/templatetags/theme_tags.py index 68a7ab4e..503482b9 100644 --- a/allianceauth/theme/templatetags/theme_tags.py +++ b/allianceauth/theme/templatetags/theme_tags.py @@ -58,7 +58,7 @@ def theme_html_tags(context): def header_padding_size(context): request = context['request'] theme = get_theme(request) - return getattr(theme, "header_padding") + return theme.header_padding @register.inclusion_tag('theme/theme_imports_js.html', takes_context=True) diff --git a/allianceauth/thirdparty/navhelper/templatetags/navactive.py b/allianceauth/thirdparty/navhelper/templatetags/navactive.py index f9b34610..d54028b4 100644 --- a/allianceauth/thirdparty/navhelper/templatetags/navactive.py +++ b/allianceauth/thirdparty/navhelper/templatetags/navactive.py @@ -21,11 +21,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import re + +from django.conf import settings from django.template import Library from django.urls import resolve from django.urls.exceptions import Resolver404 -from django.conf import settings -import re register = Library() diff --git a/allianceauth/thirdparty/navhelper/tests.py b/allianceauth/thirdparty/navhelper/tests.py index afe9d878..4b580195 100644 --- a/allianceauth/thirdparty/navhelper/tests.py +++ b/allianceauth/thirdparty/navhelper/tests.py @@ -24,7 +24,7 @@ THE SOFTWARE. # -*- coding: utf-8 -*- from django.template import Context, Template -from django.test import TestCase, RequestFactory +from django.test import RequestFactory, TestCase class NavhelperTemplateTagTests(TestCase): diff --git a/allianceauth/timerboard/auth_hooks.py b/allianceauth/timerboard/auth_hooks.py index 68bb2a6f..2fa01acb 100644 --- a/allianceauth/timerboard/auth_hooks.py +++ b/allianceauth/timerboard/auth_hooks.py @@ -1,8 +1,8 @@ +from allianceauth import hooks from allianceauth.menu.hooks import MenuItemHook from allianceauth.services.hooks import UrlHook - -from allianceauth import hooks from allianceauth.timerboard.views import dashboard_timers + from . import urls diff --git a/allianceauth/timerboard/form.py b/allianceauth/timerboard/form.py index 52aab92d..2bab4051 100644 --- a/allianceauth/timerboard/form.py +++ b/allianceauth/timerboard/form.py @@ -23,7 +23,7 @@ class TimerForm(forms.ModelForm): # for appropriate fields current_time = timezone.now() td = kwargs['instance'].eve_time - current_time - initial = kwargs.pop('initial', dict()) + initial = kwargs.pop('initial', {}) if 'days_left' not in initial: initial.update({'days_left': td.days}) if 'hours_left' not in initial: diff --git a/allianceauth/timerboard/migrations/0001_initial.py b/allianceauth/timerboard/migrations/0001_initial.py index 6f8d5e1c..6f434eb3 100644 --- a/allianceauth/timerboard/migrations/0001_initial.py +++ b/allianceauth/timerboard/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 1.10.1 on 2016-09-05 21:40 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/timerboard/migrations/0003_on_delete.py b/allianceauth/timerboard/migrations/0003_on_delete.py index 1a3013b0..765afc34 100644 --- a/allianceauth/timerboard/migrations/0003_on_delete.py +++ b/allianceauth/timerboard/migrations/0003_on_delete.py @@ -1,8 +1,8 @@ # Generated by Django 1.11.5 on 2017-09-28 02:16 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/allianceauth/timerboard/models.py b/allianceauth/timerboard/models.py index d4127c9b..075f1383 100644 --- a/allianceauth/timerboard/models.py +++ b/allianceauth/timerboard/models.py @@ -2,8 +2,7 @@ from django.contrib.auth.models import User from django.db import models from django.utils.translation import gettext as _ -from allianceauth.eveonline.models import EveCharacter -from allianceauth.eveonline.models import EveCorporationInfo +from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo class Timer(models.Model): @@ -70,8 +69,8 @@ class Timer(models.Model): corp_timer = models.BooleanField(default=False) user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - def __str__(self) -> str: - return str(self.system) + ' ' + str(self.details) - class Meta: ordering = ['eve_time'] + + def __str__(self) -> str: + return str(self.system) + ' ' + str(self.details) diff --git a/allianceauth/timerboard/tests.py b/allianceauth/timerboard/tests.py index 26e7ef00..98a54678 100644 --- a/allianceauth/timerboard/tests.py +++ b/allianceauth/timerboard/tests.py @@ -1,25 +1,26 @@ -from django_webtest import WebTest -from django.utils import timezone -from django.urls import reverse -from django.contrib.auth.models import Permission, User -from django.conf import settings - from datetime import timedelta -from allianceauth.tests.auth_utils import AuthUtils -from allianceauth.eveonline.models import EveCorporationInfo +from django_webtest import WebTest + +from django.conf import settings +from django.contrib.auth.models import Permission, User +from django.urls import reverse +from django.utils import timezone + +from allianceauth.eveonline.models import EveCorporationInfo +from allianceauth.tests.auth_utils import AuthUtils -from .models import Timer from .form import TimerForm +from .models import Timer class TimerboardViewsTestCase(WebTest): csrf_checks = False def setUp(self): - corp = EveCorporationInfo.objects.create(corporation_id='2345', corporation_name='test corp', + EveCorporationInfo.objects.create(corporation_id='2345', corporation_name='test corp', corporation_ticker='testc', member_count=24) - other_corp = EveCorporationInfo.objects.create(corporation_id='9345', corporation_name='other test corp', + EveCorporationInfo.objects.create(corporation_id='9345', corporation_name='other test corp', corporation_ticker='testd', member_count=1) self.user = AuthUtils.create_user('test_user') AuthUtils.add_main_character(self.user, 'test character', '1234', '2345', 'test corp', 'testc') diff --git a/allianceauth/timerboard/views.py b/allianceauth/timerboard/views.py index 93c877c8..b4695f73 100644 --- a/allianceauth/timerboard/views.py +++ b/allianceauth/timerboard/views.py @@ -1,12 +1,12 @@ -import datetime import logging from django.contrib import messages from django.contrib.auth.mixins import ( - LoginRequiredMixin, PermissionRequiredMixin, + LoginRequiredMixin, + PermissionRequiredMixin, ) from django.db.models import Q -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils import timezone diff --git a/allianceauth/urls.py b/allianceauth/urls.py index a2505f4d..6d1cc12d 100644 --- a/allianceauth/urls.py +++ b/allianceauth/urls.py @@ -1,13 +1,12 @@ -from typing import List -from collections.abc import Iterable, Callable +from collections.abc import Callable, Iterable -from django.urls import include -import esi.urls from django.conf import settings from django.contrib import admin from django.urls import URLPattern, include, path from django.views.generic.base import TemplateView +import esi.urls + import allianceauth.authentication.urls import allianceauth.authentication.views import allianceauth.groupmanagement.urls @@ -15,10 +14,7 @@ import allianceauth.notifications.urls import allianceauth.services.urls from allianceauth import NAME, views from allianceauth.authentication import hmac_urls -from allianceauth.authentication.decorators import ( - decorate_url_patterns, - main_character_required -) +from allianceauth.authentication.decorators import decorate_url_patterns, main_character_required from allianceauth.hooks import get_hooks admin.site.site_header = NAME diff --git a/allianceauth/utils/counters.py b/allianceauth/utils/counters.py index c84c5527..ed28b134 100644 --- a/allianceauth/utils/counters.py +++ b/allianceauth/utils/counters.py @@ -1,6 +1,5 @@ """Counters.""" -from typing import Optional from redis import Redis diff --git a/allianceauth/utils/testing.py b/allianceauth/utils/testing.py index 43afdf58..5b4bbb13 100644 --- a/allianceauth/utils/testing.py +++ b/allianceauth/utils/testing.py @@ -1,4 +1,5 @@ import socket + from django.test import TestCase diff --git a/allianceauth/utils/tests/test_testing.py b/allianceauth/utils/tests/test_testing.py index ee8ccbbc..2b569aaa 100644 --- a/allianceauth/utils/tests/test_testing.py +++ b/allianceauth/utils/tests/test_testing.py @@ -1,4 +1,5 @@ import requests + from allianceauth.utils.testing import NoSocketsTestCase, SocketAccessError diff --git a/docker/conf/celery.py b/docker/conf/celery.py index 5129f00a..6d0c5a8e 100644 --- a/docker/conf/celery.py +++ b/docker/conf/celery.py @@ -1,11 +1,12 @@ import os + from celery import Celery from celery.app import trace # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myauth.settings.local') -from django.conf import settings # noqa +from django.conf import settings app = Celery('myauth') diff --git a/docker/conf/urls.py b/docker/conf/urls.py index 58d40642..a098e831 100644 --- a/docker/conf/urls.py +++ b/docker/conf/urls.py @@ -1,6 +1,7 @@ -from allianceauth import urls from django.urls import include, path +from allianceauth import urls + urlpatterns = [ path('', include(urls)), ] diff --git a/docs/conf.py b/docs/conf.py index 78f8c63b..8fca5e2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,9 +12,11 @@ # import os import sys -import django + import sphinx_rtd_theme # noqa +import django + sys.path.insert(0, os.path.abspath('..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings_all' django.setup() diff --git a/manage.py b/manage.py index 6afed5b2..69124cd4 100644 --- a/manage.py +++ b/manage.py @@ -1,22 +1,20 @@ #!/usr/bin/env python -import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.project_template.project_name.settings.base") try: from django.core.management import execute_from_command_line - except ImportError: + except ImportError as err: # The above import may fail for some other reason. Ensure that the # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django + import django # noqa: F401 except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) + ) from err raise execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index 18b47caa..65061af6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,9 @@ lint.select = [ lint.ignore = [ "E501", # Line too long, WIP across repo. ] + +lint.per-file-ignores = { "*local.py" = [ "F405", "F403" ] } + lint.isort.combine-as-imports = true # profile=django lint.isort.section-order = [ "future", @@ -166,6 +169,9 @@ select = [ ignore = [ "E501", # Line too long, WIP across repo. ] +per-file-ignores = [ + "docker/conf/local.py:F405", +] [tool.djlint] max_attribute_length = 119 diff --git a/runtests.py b/runtests.py index 35d797b7..7aadd287 100644 --- a/runtests.py +++ b/runtests.py @@ -1,21 +1,21 @@ #!/usr/bin/env python -import os import sys if __name__ == "__main__": try: from django.core.management import execute_from_command_line - except ImportError: + except ImportError as err: # The above import may fail for some other reason. Ensure that the # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django + import django # noqa: F401 except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) - raise - execute_from_command_line(sys.argv.insert(1, 'test')) + ) from err # Provide context for the original error + raise # Re-raise original exception with context + + execute_from_command_line(sys.argv.insert(1, "test")) diff --git a/tests/celery.py b/tests/celery.py index dfbc2779..ed1c7996 100644 --- a/tests/celery.py +++ b/tests/celery.py @@ -5,7 +5,7 @@ from celery import Celery # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') -from django.conf import settings # noqa +from django.conf import settings app = Celery('devauth') diff --git a/tests/settings_all.py b/tests/settings_all.py index 5bddee70..2c6f6d14 100644 --- a/tests/settings_all.py +++ b/tests/settings_all.py @@ -4,12 +4,12 @@ Alliance Auth Test Suite Django settings Testing all services and plug-in apps """ -from allianceauth.project_template.project_name.settings.base import * +from allianceauth.project_template.project_name.settings.base import * # noqa:F403 # Celery configuration CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing -INSTALLED_APPS += [ +INSTALLED_APPS += [ # noqa:F405 "allianceauth.eveonline.autogroups", "allianceauth.hrapplications", "allianceauth.timerboard", diff --git a/tests/settings_core.py b/tests/settings_core.py index 6403cfa8..ed72dd13 100644 --- a/tests/settings_core.py +++ b/tests/settings_core.py @@ -4,7 +4,7 @@ Alliance Auth Test Suite Django settings Testing core packages only """ -from allianceauth.project_template.project_name.settings.base import * +from allianceauth.project_template.project_name.settings.base import * # noqa:F403 # Celery configuration CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing diff --git a/tests/urls.py b/tests/urls.py index ba840b33..d271ad7c 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,5 +1,7 @@ -import allianceauth.urls from django.urls import path + +import allianceauth.urls + from . import views urlpatterns = allianceauth.urls.urlpatterns From c4cbaac454476aea1229ae4224100818e35deb4b Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Fri, 13 Sep 2024 23:19:30 +1000 Subject: [PATCH 017/178] Add Git Blame Ignore --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..3a6bc90f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# git config blame.ignoreRevsFile .git-blame-ignore-revs + + + +# Ruff initial formatting storm +a99315ea55339f0b6010b5c9d8703e51278fcf29 From ae16a3de81334a9c397bd520c244d54c1c8f23ad Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:20:29 +1000 Subject: [PATCH 018/178] Datatables 2.x --- allianceauth/templates/bundles/datatables-css-bs5.html | 2 +- allianceauth/templates/bundles/datatables-js-bs5.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/allianceauth/templates/bundles/datatables-css-bs5.html b/allianceauth/templates/bundles/datatables-css-bs5.html index 4f015af9..c7c3768c 100644 --- a/allianceauth/templates/bundles/datatables-css-bs5.html +++ b/allianceauth/templates/bundles/datatables-css-bs5.html @@ -1,3 +1,3 @@ - + diff --git a/allianceauth/templates/bundles/datatables-js-bs5.html b/allianceauth/templates/bundles/datatables-js-bs5.html index 83691ab5..e44f62c9 100644 --- a/allianceauth/templates/bundles/datatables-js-bs5.html +++ b/allianceauth/templates/bundles/datatables-js-bs5.html @@ -1,4 +1,4 @@ - - + + From c0cc927788ce34b158b25723813cf84e519a6568 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:21:19 +1000 Subject: [PATCH 019/178] Fontawesome 6.6 --- allianceauth/templates/bundles/fontawesome.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/templates/bundles/fontawesome.html b/allianceauth/templates/bundles/fontawesome.html index 75191f62..7a47921c 100644 --- a/allianceauth/templates/bundles/fontawesome.html +++ b/allianceauth/templates/bundles/fontawesome.html @@ -1,3 +1,3 @@ - + From c9bee08a6ebb88e8c302aa158421cf28ef72f37d Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:21:36 +1000 Subject: [PATCH 020/178] moment.js 2.30.1 --- allianceauth/templates/bundles/moment-js.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/templates/bundles/moment-js.html b/allianceauth/templates/bundles/moment-js.html index a094b4be..cfcf6f64 100644 --- a/allianceauth/templates/bundles/moment-js.html +++ b/allianceauth/templates/bundles/moment-js.html @@ -1,7 +1,7 @@ - + {% if locale and LANGUAGE_CODE != 'en' %} - + {% endif %} From 23259e919c801e00ac2b277e44e227652f3fa783 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:22:34 +1000 Subject: [PATCH 021/178] jqueryui 1.14 --- .../allianceauth/js/jquery-ui/1.14.0/css/jquery-ui.min.css | 6 ++++++ allianceauth/templates/bundles/jquery-ui-css.html | 2 +- allianceauth/templates/bundles/jquery-ui-js.html | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 allianceauth/static/allianceauth/js/jquery-ui/1.14.0/css/jquery-ui.min.css diff --git a/allianceauth/static/allianceauth/js/jquery-ui/1.14.0/css/jquery-ui.min.css b/allianceauth/static/allianceauth/js/jquery-ui/1.14.0/css/jquery-ui.min.css new file mode 100644 index 00000000..46642fe0 --- /dev/null +++ b/allianceauth/static/allianceauth/js/jquery-ui/1.14.0/css/jquery-ui.min.css @@ -0,0 +1,6 @@ +/*! jQuery UI - v1.14.0 - 2024-09-13 +* https://jqueryui.com +* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ + +.ui-draggable-handle{touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;user-select:none}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px} \ No newline at end of file diff --git a/allianceauth/templates/bundles/jquery-ui-css.html b/allianceauth/templates/bundles/jquery-ui-css.html index 3bf4d1a3..b038fb06 100644 --- a/allianceauth/templates/bundles/jquery-ui-css.html +++ b/allianceauth/templates/bundles/jquery-ui-css.html @@ -1,5 +1,5 @@ {% load static %} - + diff --git a/allianceauth/templates/bundles/jquery-ui-js.html b/allianceauth/templates/bundles/jquery-ui-js.html index e9e91eae..7dc99461 100644 --- a/allianceauth/templates/bundles/jquery-ui-js.html +++ b/allianceauth/templates/bundles/jquery-ui-js.html @@ -1,3 +1,3 @@ - + From f54fc26a1c94ba04df27d15bf46087dfac799b40 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:24:07 +1000 Subject: [PATCH 022/178] BS 5.3.3 (for login page) --- allianceauth/templates/bundles/bootstrap-css-bs5.html | 6 +++--- allianceauth/templates/bundles/bootstrap-js-bs5.html | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/allianceauth/templates/bundles/bootstrap-css-bs5.html b/allianceauth/templates/bundles/bootstrap-css-bs5.html index 308ded67..9d080d76 100644 --- a/allianceauth/templates/bundles/bootstrap-css-bs5.html +++ b/allianceauth/templates/bundles/bootstrap-css-bs5.html @@ -1,4 +1,4 @@ {% load static %} - - - + + + diff --git a/allianceauth/templates/bundles/bootstrap-js-bs5.html b/allianceauth/templates/bundles/bootstrap-js-bs5.html index 6f60d2e3..6fadf342 100644 --- a/allianceauth/templates/bundles/bootstrap-js-bs5.html +++ b/allianceauth/templates/bundles/bootstrap-js-bs5.html @@ -1,4 +1,3 @@ - -{##} - - + + + From dd255664d4e959a2f7544a78656b6a624310f030 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:45:33 +1000 Subject: [PATCH 023/178] Move to Mariadb 11.4 LTS --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1c427652..a201dfb8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -44,7 +44,7 @@ x-allianceauth-health-check: &allianceauth-health-checks services: auth_mysql: - image: mariadb:10.11 + image: mariadb:11.4 command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --default-authentication-plugin=mysql_native_password] volumes: - ./mysql-data:/var/lib/mysql From 4bda887234cd46c1918dd5f59f3a01afcb94cb29 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:45:56 +1000 Subject: [PATCH 024/178] Use Python 3.12 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 61801557..20799743 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM python:3.12-slim ARG AUTH_VERSION=v4.3.1 ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION} ENV AUTH_USER=allianceauth From 30bb855381c8e000b343e6bb9809f9d7fe25bc95 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:46:52 +1000 Subject: [PATCH 025/178] bump infra --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2c0e7f76..440f3916 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ before_script: pre-commit-check: <<: *only-default stage: pre-commit - image: python:3.11-bookworm + image: python:3.12-bookworm # variables: # PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit # cache: @@ -193,7 +193,7 @@ deploy_production: build-image: before_script: [] - image: docker:27.0 + image: docker:27 stage: docker services: - docker:27-dind From cc11761d8e93ad653b256e3bbefd4aac43e05125 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:47:10 +1000 Subject: [PATCH 026/178] docker-compose version is deprecated --- docker/docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a201dfb8..479422e8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - x-allianceauth-base: &allianceauth-base image: ${AA_DOCKER_TAG?err} # build: @@ -131,7 +129,7 @@ services: replicas: 2 grafana: - image: grafana/grafana-oss:9.5.2 + image: grafana/grafana-oss:11.2.0 restart: always depends_on: - auth_mysql From 757c6fa491603927cff994acfdf8401a878a4856 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Mon, 16 Sep 2024 11:59:35 +1000 Subject: [PATCH 027/178] Drop BS3 templates --- .../authentication/dashboard_bs3.html | 10 --- .../css/themes/bootstrap-locals.less | 10 --- .../allianceauth/css/themes/darkly/LICENSE | 21 ------ .../css/themes/darkly/darkly.less | 9 --- .../css/themes/darkly/darkly.min.css | 5 -- .../css/themes/flatly-shared.less | 31 -------- .../allianceauth/css/themes/flatly/LICENSE | 21 ------ .../css/themes/flatly/flatly.less | 35 --------- .../css/themes/flatly/flatly.min.css | 5 -- allianceauth/templates/allianceauth/base.html | 64 ----------------- .../templates/allianceauth/messages.html | 27 ------- .../templates/allianceauth/night-toggle.html | 8 --- .../allianceauth/top-menu-user-dropdown.html | 72 ------------------- .../templates/allianceauth/top-menu.html | 36 ---------- .../templates/bundles/bootstrap-css.html | 20 ------ .../templates/bundles/bootstrap-js.html | 4 -- .../templates/bundles/datatables-css.html | 3 - .../templates/bundles/datatables-js.html | 4 -- docs/customizing/index.md | 2 +- 19 files changed, 1 insertion(+), 386 deletions(-) delete mode 100644 allianceauth/authentication/templates/authentication/dashboard_bs3.html delete mode 100644 allianceauth/static/allianceauth/css/themes/bootstrap-locals.less delete mode 100644 allianceauth/static/allianceauth/css/themes/darkly/LICENSE delete mode 100644 allianceauth/static/allianceauth/css/themes/darkly/darkly.less delete mode 100644 allianceauth/static/allianceauth/css/themes/darkly/darkly.min.css delete mode 100644 allianceauth/static/allianceauth/css/themes/flatly-shared.less delete mode 100644 allianceauth/static/allianceauth/css/themes/flatly/LICENSE delete mode 100644 allianceauth/static/allianceauth/css/themes/flatly/flatly.less delete mode 100644 allianceauth/static/allianceauth/css/themes/flatly/flatly.min.css delete mode 100644 allianceauth/templates/allianceauth/base.html delete mode 100644 allianceauth/templates/allianceauth/messages.html delete mode 100644 allianceauth/templates/allianceauth/night-toggle.html delete mode 100644 allianceauth/templates/allianceauth/top-menu-user-dropdown.html delete mode 100644 allianceauth/templates/allianceauth/top-menu.html delete mode 100644 allianceauth/templates/bundles/bootstrap-css.html delete mode 100644 allianceauth/templates/bundles/bootstrap-js.html delete mode 100644 allianceauth/templates/bundles/datatables-css.html delete mode 100644 allianceauth/templates/bundles/datatables-js.html diff --git a/allianceauth/authentication/templates/authentication/dashboard_bs3.html b/allianceauth/authentication/templates/authentication/dashboard_bs3.html deleted file mode 100644 index 14bb8cbc..00000000 --- a/allianceauth/authentication/templates/authentication/dashboard_bs3.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'allianceauth/base.html' %} - - -{% block page_title %}Dashboard{% endblock page_title %} - -{% block content %} -
-

Dashboard Dummy

-
-{% endblock %} diff --git a/allianceauth/static/allianceauth/css/themes/bootstrap-locals.less b/allianceauth/static/allianceauth/css/themes/bootstrap-locals.less deleted file mode 100644 index 7a5b01ea..00000000 --- a/allianceauth/static/allianceauth/css/themes/bootstrap-locals.less +++ /dev/null @@ -1,10 +0,0 @@ -// Import the fonts from CDN -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot'); - src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), - url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2') format('woff2'), - url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff') format('woff'), - url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf') format('truetype'), - url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg'); -} diff --git a/allianceauth/static/allianceauth/css/themes/darkly/LICENSE b/allianceauth/static/allianceauth/css/themes/darkly/LICENSE deleted file mode 100644 index c470246f..00000000 --- a/allianceauth/static/allianceauth/css/themes/darkly/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Thomas Park - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/allianceauth/static/allianceauth/css/themes/darkly/darkly.less b/allianceauth/static/allianceauth/css/themes/darkly/darkly.less deleted file mode 100644 index 0b89d10e..00000000 --- a/allianceauth/static/allianceauth/css/themes/darkly/darkly.less +++ /dev/null @@ -1,9 +0,0 @@ -// Alliance Auth customisations of the bootswatch Darkly theme -// To build a new CSS file you need to `npm install -g less less-plugin-clean-css` -// Then `lessc --clean-css="--advanced" darkly.less darkly.min.css` - -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less"; -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/darkly/variables.less"; -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/fa207fbbc80bd74339e179b054a322b092be98f6/darkly/bootswatch.less"; -@import "../bootstrap-locals.less"; -@import "../flatly-shared.less"; diff --git a/allianceauth/static/allianceauth/css/themes/darkly/darkly.min.css b/allianceauth/static/allianceauth/css/themes/darkly/darkly.min.css deleted file mode 100644 index bd20df07..00000000 --- a/allianceauth/static/allianceauth/css/themes/darkly/darkly.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic);hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.glyphicon,address{font-style:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.img-thumbnail,body{background-color:#222}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.42857143;color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#89be89;text-decoration:none}a:focus,a:hover{color:#89be89;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:2px;line-height:1.42857143;border:1px solid #464545;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border-top:1px solid #464545}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:400;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#999}.h1,.h2,.h3,h1,h2,h3{margin-top:21px;margin-bottom:10.5px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10.5px;margin-bottom:10.5px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:39px}.h2,h2{font-size:32px}.h3,h3{font-size:26px}.h4,h4{font-size:19px}.h5,h5{font-size:15px}.h6,h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:22.5px}}.small,small{font-size:86%}.mark,mark{padding:.2em;background-color:#f0ad4e}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999}a.text-primary:focus,a.text-primary:hover{color:#28415b}a.text-danger:focus,a.text-danger:hover,a.text-info:focus,a.text-info:hover,a.text-success:focus,a.text-success:hover,a.text-warning:focus,a.text-warning:hover{color:#e6e6e6}.bg-primary{color:#fff;background-color:#375a7f}a.bg-primary:focus,a.bg-primary:hover{background-color:#28415b}.bg-success{background-color:#5cb85c}a.bg-success:focus,a.bg-success:hover{background-color:#449d44}.bg-info{background-color:#3498DB}a.bg-info:focus,a.bg-info:hover{background-color:#217dbb}.bg-warning{background-color:#f0ad4e}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f}.bg-danger{background-color:#d9534f}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c}pre code,table{background-color:transparent}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid transparent}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:21px}ol,ul{margin-bottom:10.5px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}legend,pre{display:block}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help}.checkbox.disabled label,.form-control[disabled],.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .form-control,fieldset[disabled] .radio label,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #464545}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#999}kbd,legend{color:#fff}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #464545;border-left:0}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:10px;margin:0 0 10.5px;font-size:14px;color:#303030;word-break:break-all;word-wrap:break-word;background-color:#EBEBEB;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.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-md-1,.col-md-10,.col-md-11,.col-md-12,.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-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.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-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999}.table{width:100%;max-width:100%;margin-bottom:21px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #464545}.table>thead>tr>th{vertical-align:bottom}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #464545}.table .table{background-color:#222}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #464545}.table-striped>tbody>tr:nth-of-type(odd){background-color:#3d3d3d}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#464545}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#393838}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#5cb85c}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#4cae4c}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#3498DB}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#258cd1}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#f0ad4e}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#eea236}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#d9534f}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#d43f3a}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #464545}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{min-width:0;margin:0}legend{width:100%;margin-bottom:21px;font-size:22.5px;line-height:inherit;border-bottom:1px solid transparent}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:15px;line-height:1.42857143;color:#464545;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:11px}.form-control{width:100%;height:45px;padding:10px 15px;background-color:#fff;background-image:none;border-radius:4px;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#fff;outline:0}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#EBEBEB;opacity:1}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:45px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:35px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:66px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.checkbox-inline,.radio label,.radio-inline{padding-left:20px;cursor:pointer;margin-bottom:0;font-weight:400}.checkbox label,.radio label{min-height:21px}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;vertical-align:middle}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:36px;padding-top:11px;padding-bottom:11px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.form-group-sm .form-control,.input-sm{padding:6px 9px;border-radius:3px;font-size:13px}.input-sm{height:35px;line-height:1.5}select.input-sm{height:35px;line-height:35px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:35px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:18px 27px;font-size:19px}.form-group-sm select.form-control{height:35px;line-height:35px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:35px;min-height:34px;padding:7px 9px;font-size:13px;line-height:1.5}.input-lg{height:66px;line-height:1.3333333}select.input-lg{height:66px;line-height:66px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:66px;line-height:1.3333333}.form-group-lg select.form-control{height:66px;line-height:66px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:66px;min-height:40px;padding:19px 27px;font-size:19px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:56.25px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:45px;height:45px;line-height:45px;text-align:center;pointer-events:none}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:66px;height:66px;line-height:66px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:35px;height:35px;line-height:35px}.has-success .form-control{border-color:#fff}.has-success .form-control:focus{border-color:#e6e6e6}.has-success .input-group-addon{color:#fff;background-color:#5cb85c}.has-warning .form-control{border-color:#fff}.has-warning .form-control:focus{border-color:#e6e6e6}.has-warning .input-group-addon{color:#fff;background-color:#f0ad4e}.has-error .form-control{border-color:#fff}.has-error .form-control:focus{border-color:#e6e6e6}.has-error .input-group-addon{color:#fff;background-color:#d9534f}.has-feedback label~.form-control-feedback{top:26px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#fff}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{padding-top:11px;margin-bottom:0;text-align:right}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:11px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:32px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:19px;font-size:19px}.form-horizontal .form-group-sm .control-label{padding-top:7px;font-size:13px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:10px 15px;font-size:15px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#fff;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#fff;background-color:#464545;border-color:#464545}.btn-default.focus,.btn-default:focus{color:#fff;background-color:#2c2c2c;border-color:#060606}.btn-default:hover{color:#fff;background-color:#2c2c2c;border-color:#272727}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#fff;background-color:#2c2c2c;background-image:none;border-color:#272727}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#fff;background-color:#1a1a1a;border-color:#060606}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#464545;border-color:#464545}.btn-default .badge{color:#464545;background-color:#fff}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#28415b;border-color:#101b26}.btn-primary:hover{color:#fff;background-color:#28415b;border-color:#253c54}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#28415b;background-image:none;border-color:#253c54}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#1d2f43;border-color:#101b26}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#375a7f;border-color:#375a7f}.btn-primary .badge{color:#375a7f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#2d672d}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#2d672d}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#5cb85c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#217dbb;border-color:#16527a}.btn-info:hover{color:#fff;background-color:#217dbb;border-color:#2077b2}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#217dbb;background-image:none;border-color:#2077b2}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#1c699d;border-color:#16527a}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#3498DB;border-color:#3498DB}.btn-info .badge{color:#3498DB;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#b06d0f}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#b06d0f}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#8b211e}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#8b211e}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d9534f}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#89be89;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#89be89;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#999;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:15px;text-align:left;list-style:none;background-color:#303030;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu-right,.dropdown-menu.pull-right{right:0;left:auto}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#464545}.dropdown-menu>li>a{clear:both;font-weight:400;color:#EBEBEB}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#375a7f;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#999}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:13px;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:66px;padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:66px;line-height:66px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:35px;padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:35px;line-height:35px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:10px 15px;font-size:15px;font-weight:400;line-height:1;text-align:center;background-color:#464545;border:1px solid transparent;border-radius:4px}.input-group-addon.input-sm{padding:6px 9px;font-size:13px;border-radius:3px}.input-group-addon.input-lg{padding:18px 27px;font-size:19px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#303030}.nav>li.disabled>a{color:#605e5e}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#605e5e;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#303030}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #464545}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#464545}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#5cb85c;cursor:default;background-color:#222;border:1px solid #464545;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #EBEBEB}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #EBEBEB;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#222}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#375a7f}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #EBEBEB}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #EBEBEB;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#222}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:60px;margin-bottom:21px;border:1px solid transparent}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-brand{float:left;height:60px;padding:19.5px 15px;font-size:19px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:13px;margin-bottom:13px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:9.75px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:19.5px;padding-bottom:19.5px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:7.5px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:7.5px;margin-bottom:7.5px}.navbar-btn.btn-sm{margin-top:12.5px;margin-bottom:12.5px}.navbar-btn.btn-xs{margin-top:19px;margin-bottom:19px}.navbar-text{margin-top:19.5px;margin-bottom:19.5px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#375a7f;border-color:transparent}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#fff}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#fff;background-color:#28415b}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#fff;background-color:#28415b}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#28415b}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#28415b}.navbar-default .navbar-collapse,.navbar-default .navbar-form,.navbar-inverse{border-color:transparent}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#28415b}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-link{color:#fff}.navbar-default .navbar-link:hover{color:#5cb85c}.navbar-default .btn-link{color:#fff}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#5cb85c}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#5cb85c}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#375a7f;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#fff}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#375a7f;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#4cae4c}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#aaa;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#4cae4c}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#375a7f;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#4cae4c}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#aaa;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#449d44}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#449d44}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#49a749}.navbar-inverse .navbar-link{color:#fff}.navbar-inverse .navbar-link:hover{color:#375a7f}.navbar-inverse .btn-link{color:#fff}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#375a7f}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#aaa}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#464545;border-radius:4px}.breadcrumb>li+li:before{padding:0 5px;color:#fff;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{padding-left:0;margin:21px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:10px 15px;margin-left:-1px;line-height:1.42857143;color:#fff;text-decoration:none;background-color:#5cb85c;border:1px solid transparent}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#fff;background-color:#71c171;border-color:transparent}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#71c171;border-color:transparent}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#fff;cursor:not-allowed;background-color:#3d8b3d;border-color:transparent}.pagination-lg>li>a,.pagination-lg>li>span{padding:18px 27px;font-size:19px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:6px 9px;font-size:13px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:21px 0;text-align:center;list-style:none}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#5cb85c;border:1px solid transparent;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#71c171}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#ddd;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;color:#fff;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#464545}.label-default[href]:focus,.label-default[href]:hover{background-color:#2c2c2c}.label-primary{background-color:#375a7f}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#28415b}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#3498DB}.label-info[href]:focus,.label-info[href]:hover{background-color:#217dbb}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:13px;color:#fff;vertical-align:middle;background-color:#464545;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#375a7f;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#303030}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.alert,.thumbnail{margin-bottom:21px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#161616}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:68px}}.thumbnail{padding:2px;line-height:1.42857143;background-color:#222;border:1px solid #464545;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#89be89}.thumbnail .caption{padding:9px;color:#fff}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{top:0;right:0;bottom:0;left:0}.alert-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.alert-success hr{border-top-color:#4cae4c}.alert-success .alert-link{color:#e6e6e6}.alert-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.alert-info hr{border-top-color:#258cd1}.alert-info .alert-link{color:#e6e6e6}.alert-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.alert-warning hr{border-top-color:#eea236}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.alert-danger hr{border-top-color:#d43f3a}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{margin-bottom:21px;background-color:#EBEBEB;border-radius:4px}.progress-bar{float:left;width:0%;height:100%;font-size:13px;line-height:21px;color:#fff;text-align:center;background-color:#375a7f;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#3498DB}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#303030;border:1px solid #464545}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#999;cursor:not-allowed;background-color:#EBEBEB}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#999}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#a8c0da}a.list-group-item,button.list-group-item{color:#89be89}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#78b578}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#89be89;text-decoration:none;background-color:transparent}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#fff;background-color:#5cb85c}a.list-group-item-success,button.list-group-item-success{color:#fff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#fff;background-color:#4cae4c}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-info{color:#fff;background-color:#3498DB}a.list-group-item-info,button.list-group-item-info{color:#fff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#fff;background-color:#258cd1}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-warning{color:#fff;background-color:#f0ad4e}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#fff;background-color:#eea236}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#fff;background-color:#d9534f}a.list-group-item-danger,button.list-group-item-danger{color:#fff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#fff;background-color:#d43f3a}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#fff;border-color:#fff}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#303030;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-title{margin-top:0;font-size:17px}.panel-footer{padding:10px 15px;background-color:#464545;border-top:1px solid #464545;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #464545}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #464545}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #464545}.panel-default{border-color:#464545}.panel-default>.panel-heading{color:#fff;border-color:#464545}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#464545}.panel-default>.panel-heading .badge{color:#303030;background-color:#fff}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#464545}.panel-primary{border-color:#375a7f}.panel-primary>.panel-heading{color:#fff;background-color:#375a7f;border-color:#375a7f}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#375a7f}.panel-primary>.panel-heading .badge{color:#375a7f;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#375a7f}.panel-success{border-color:#5cb85c}.panel-success>.panel-heading{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#5cb85c}.panel-success>.panel-heading .badge{color:#5cb85c;background-color:#fff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#5cb85c}.panel-info{border-color:#3498DB}.panel-info>.panel-heading{color:#fff;background-color:#3498DB;border-color:#3498DB}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3498DB}.panel-info>.panel-heading .badge{color:#3498DB;background-color:#fff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3498DB}.panel-warning{border-color:#f0ad4e}.panel-warning>.panel-heading{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0ad4e}.panel-warning>.panel-heading .badge{color:#f0ad4e;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0ad4e}.panel-danger{border-color:#d9534f}.panel-danger>.panel-heading{color:#fff;background-color:#d9534f;border-color:#d9534f}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d9534f}.panel-danger>.panel-heading .badge{color:#d9534f;background-color:#fff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d9534f}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#303030;border:1px solid transparent;border-radius:4px}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:22.5px;line-height:1;color:#fff;filter:alpha(opacity=20)}.popover,.tooltip{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-break:auto;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;text-decoration:none}.modal-title,.popover,.tooltip{line-height:1.42857143}.close:focus,.close:hover{color:#fff;text-decoration:none;cursor:pointer;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{position:fixed;z-index:1050;display:none;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#303030;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=70);opacity:.7}.modal-header{padding:15px;border-bottom:1px solid #464545}.modal-header .close{margin-top:-2px}.modal-title{margin:0}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #464545}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:13px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:15px;background-color:#303030;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#666;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#303030;border-bottom-width:0}.popover.left>.arrow:after,.popover.right>.arrow:after{bottom:-10px;content:" "}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#666;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{left:1px;border-right-color:#303030;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#666;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#303030}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#666;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#303030}.popover-title{padding:8px 14px;margin:0;font-size:15px;background-color:#282828;border-bottom:1px solid #1c1c1c;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel-inner{width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.close,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.navbar{border-width:0}.navbar-default .badge{background-color:#fff;color:#375a7f}.navbar-inverse .badge{background-color:#fff;color:#5cb85c}.navbar-brand{line-height:1}.navbar-form .form-control{background-color:#fff}.navbar-form .form-control:focus{border-color:#fff}.btn{border-width:2px}.btn-group.open .dropdown-toggle,.btn:active{-webkit-box-shadow:none;box-shadow:none}.text-primary,.text-primary:hover{color:#4673a3}.text-success,.text-success:hover{color:#5cb85c}.text-danger,.text-danger:hover{color:#d9534f}.text-warning,.text-warning:hover{color:#f0ad4e}.text-info,.text-info:hover{color:#3498DB}.table a:not(.btn),table a:not(.btn){text-decoration:underline}.close,.table .dropdown-menu a,table .dropdown-menu a{text-decoration:none}.table .danger,.table .danger>a,.table .danger>td>a,.table .danger>th>a,.table .info,.table .info>a,.table .info>td>a,.table .info>th>a,.table .success,.table .success>a,.table .success>td>a,.table .success>th>a,.table .warning,.table .warning>a,.table .warning>td>a,.table .warning>th>a,table .danger,table .danger>a,table .danger>td>a,table .danger>th>a,table .info,table .info>a,table .info>td>a,table .info>th>a,table .success,table .success>a,table .success>td>a,table .success>th>a,table .warning,table .warning>a,table .warning>td>a,table .warning>th>a{color:#fff}.form-control-feedback,input,textarea{color:#464545}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th,table>tbody>tr>td,table>tbody>tr>th,table>tfoot>tr>td,table>tfoot>tr>th,table>thead>tr>td,table>thead>tr>th{border:none}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th,table-bordered>tbody>tr>td,table-bordered>tbody>tr>th,table-bordered>tfoot>tr>td,table-bordered>tfoot>tr>th,table-bordered>thead>tr>td,table-bordered>thead>tr>th{border:1px solid #464545}.form-control,input,textarea{border:2px hidden transparent;-webkit-box-shadow:none;box-shadow:none}.form-control:focus,input:focus,textarea:focus{-webkit-box-shadow:none;box-shadow:none}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#f0ad4e}.has-warning .form-control,.has-warning .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.has-warning .input-group-addon{border-color:#f0ad4e}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#d9534f}.has-error .form-control,.has-error .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.has-error .input-group-addon{border-color:#d9534f}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#5cb85c}.breadcrumb a,.input-group-addon,.nav-pills>li>a,.nav-tabs>li>a,.pager a,.pager a:hover,.popover{color:#fff}.has-success .form-control,.has-success .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.has-success .input-group-addon{border-color:#5cb85c}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover,a.list-group-item.active,a.list-group-item.active:focus,a.list-group-item.active:hover{border-color:#464545}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{background-color:#3d8b3d}.close{opacity:.4}.close:focus,.close:hover{opacity:1}.alert .alert-link{color:#fff;text-decoration:underline}.progress{height:10px;-webkit-box-shadow:none;box-shadow:none}.progress .progress-bar{font-size:10px;line-height:10px}.well{-webkit-box-shadow:none;box-shadow:none}a.list-group-item-success.active{background-color:#5cb85c}a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{background-color:#4cae4c}a.list-group-item-warning.active{background-color:#f0ad4e}a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{background-color:#eea236}a.list-group-item-danger.active{background-color:#d9534f}a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{background-color:#d43f3a}.panel-default>.panel-heading{background-color:#464545}@font-face{font-family:'Glyphicons Halflings';src:url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot);src:url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff) format('woff'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:rgba(70,69,69,.2)}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:rgba(57,56,56,.2)}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:rgba(92,184,92,.2)}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:rgba(76,174,76,.2)}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:rgba(52,152,219,.2)}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:rgba(37,140,209,.2)}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:rgba(240,173,78,.2)}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:rgba(238,162,54,.2)}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:rgba(217,83,79,.2)}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:rgba(212,63,58,.2)}.row.vertical-flexbox-row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;flex-wrap:wrap}.row.vertical-flexbox-row>[class*=col-]{display:flex;flex-direction:column} \ No newline at end of file diff --git a/allianceauth/static/allianceauth/css/themes/flatly-shared.less b/allianceauth/static/allianceauth/css/themes/flatly-shared.less deleted file mode 100644 index 9ced2dcb..00000000 --- a/allianceauth/static/allianceauth/css/themes/flatly-shared.less +++ /dev/null @@ -1,31 +0,0 @@ -// Shared variables between flatly and darkly themes - -// Fade down table row colors to prevent them looking like a bag of skittles -@table-bg-fade-percent: 20%; - -.table-row-variant(active; fade(@table-bg-active, @table-bg-fade-percent)); -.table-row-variant(success; fade(@state-success-bg, @table-bg-fade-percent)); -.table-row-variant(info; fade(@state-info-bg, @table-bg-fade-percent)); -.table-row-variant(warning; fade(@state-warning-bg, @table-bg-fade-percent)); -.table-row-variant(danger; fade(@state-danger-bg, @table-bg-fade-percent)); - - -// From bootswatch Superhero -@brand-success: #5cb85c; -@brand-warning: #f0ad4e; -@brand-danger: #d9534f; - - -// Bootstrap extension, allow vertical columns to be same height -.row.vertical-flexbox-row { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - flex-wrap: wrap; -} - -.row.vertical-flexbox-row > [class*='col-'] { - display: flex; - flex-direction: column; -} diff --git a/allianceauth/static/allianceauth/css/themes/flatly/LICENSE b/allianceauth/static/allianceauth/css/themes/flatly/LICENSE deleted file mode 100644 index c470246f..00000000 --- a/allianceauth/static/allianceauth/css/themes/flatly/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Thomas Park - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/allianceauth/static/allianceauth/css/themes/flatly/flatly.less b/allianceauth/static/allianceauth/css/themes/flatly/flatly.less deleted file mode 100644 index 09f42a50..00000000 --- a/allianceauth/static/allianceauth/css/themes/flatly/flatly.less +++ /dev/null @@ -1,35 +0,0 @@ -// Alliance Auth customisations of the bootswatch Flatly theme -// To build a new CSS file you need to `npm install -g less less-plugin-clean-css` -// Then `lessc --clean-css="--advanced" flatly.less flatly.min.css` - -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less"; -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/flatly/variables.less"; -@import "https://raw.githubusercontent.com/thomaspark/bootswatch/fa207fbbc80bd74339e179b054a322b092be98f6/flatly/bootswatch.less"; -@import "../bootstrap-locals.less"; -@import "../flatly-shared.less"; - -.nav-tabs > li > a, -.nav-pills > li > a { - color: #000; -} - -// From bootswatch Superhero -@brand-info: #5bc0de; - -@table-bg-fade-percent: 40%; - -// Fixup table colors to match fade -.table { - .active, - .success, - .info, - .warning, - .danger { - color: @gray-darker; - > td { - > a:not(.btn) { - color: @link-color; - } - } - } -} diff --git a/allianceauth/static/allianceauth/css/themes/flatly/flatly.min.css b/allianceauth/static/allianceauth/css/themes/flatly/flatly.min.css deleted file mode 100644 index 6a9290e1..00000000 --- a/allianceauth/static/allianceauth/css/themes/flatly/flatly.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic);hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.glyphicon,address{font-style:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.img-thumbnail,body{background-color:#fff}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.42857143;color:#2C3E50}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#5cb85c;text-decoration:none}a:focus,a:hover{color:#5cb85c;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;border:1px solid #ecf0f1;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border-top:1px solid #ecf0f1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:400;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#b4bcc2}.h1,.h2,.h3,h1,h2,h3{margin-top:21px;margin-bottom:10.5px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10.5px;margin-bottom:10.5px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:39px}.h2,h2{font-size:32px}.h3,h3{font-size:26px}.h4,h4{font-size:19px}.h5,h5{font-size:15px}.h6,h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:22.5px}}.small,small{font-size:86%}.mark,mark{padding:.2em;background-color:#f0ad4e}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#b4bcc2}.text-primary{color:#2C3E50}a.text-primary:focus,a.text-primary:hover{color:#1a242f}a.text-danger:focus,a.text-danger:hover,a.text-info:focus,a.text-info:hover,a.text-success:focus,a.text-success:hover,a.text-warning:focus,a.text-warning:hover{color:#e6e6e6}.bg-primary{color:#fff;background-color:#2C3E50}a.bg-primary:focus,a.bg-primary:hover{background-color:#1a242f}.bg-success{background-color:#5cb85c}a.bg-success:focus,a.bg-success:hover{background-color:#449d44}.bg-info{background-color:#5bc0de}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5}.bg-warning{background-color:#f0ad4e}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f}.bg-danger{background-color:#d9534f}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c}pre code,table{background-color:transparent}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid transparent}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:21px}ol,ul{margin-bottom:10.5px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}legend,pre{display:block}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help}.checkbox.disabled label,.form-control[disabled],.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .form-control,fieldset[disabled] .radio label,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #ecf0f1}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#b4bcc2}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #ecf0f1;border-left:0}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:10px;margin:0 0 10.5px;font-size:14px;color:#7b8a8b;word-break:break-all;word-wrap:break-word;background-color:#ecf0f1;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.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-md-1,.col-md-10,.col-md-11,.col-md-12,.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-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.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-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#b4bcc2}.table{width:100%;max-width:100%;margin-bottom:21px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ecf0f1}.table>thead>tr>th{vertical-align:bottom}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ecf0f1}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ecf0f1}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#ecf0f1}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#dde4e6}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#5cb85c}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#4cae4c}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#5bc0de}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#46b8da}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#f0ad4e}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#eea236}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#d9534f}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#d43f3a}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ecf0f1}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{min-width:0;margin:0}legend{width:100%;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#2C3E50;border-bottom:1px solid transparent}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:15px;line-height:1.42857143;color:#2C3E50;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:11px}.form-control{width:100%;height:45px;padding:10px 15px;background-color:#fff;background-image:none;border:1px solid #dce4ec;border-radius:4px;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#2C3E50;outline:0}.form-control::-moz-placeholder{color:#acb6c0;opacity:1}.form-control:-ms-input-placeholder{color:#acb6c0}.form-control::-webkit-input-placeholder{color:#acb6c0}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#ecf0f1;opacity:1}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:45px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:35px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:66px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.checkbox-inline,.radio label,.radio-inline{padding-left:20px;cursor:pointer;margin-bottom:0;font-weight:400}.checkbox label,.radio label{min-height:21px}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;vertical-align:middle}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:36px;padding-top:11px;padding-bottom:11px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.form-group-sm .form-control,.input-sm{padding:6px 9px;border-radius:3px;font-size:13px}.input-sm{height:35px;line-height:1.5}select.input-sm{height:35px;line-height:35px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:35px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:18px 27px;font-size:19px}.form-group-sm select.form-control{height:35px;line-height:35px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:35px;min-height:34px;padding:7px 9px;font-size:13px;line-height:1.5}.input-lg{height:66px;line-height:1.3333333}select.input-lg{height:66px;line-height:66px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:66px;line-height:1.3333333}.form-group-lg select.form-control{height:66px;line-height:66px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:66px;min-height:40px;padding:19px 27px;font-size:19px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:56.25px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:45px;height:45px;line-height:45px;text-align:center;pointer-events:none}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:66px;height:66px;line-height:66px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:35px;height:35px;line-height:35px}.has-success .form-control{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff}.has-success .input-group-addon{color:#fff;background-color:#5cb85c}.has-warning .form-control{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#f0ad4e}.has-error .form-control{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #fff}.has-error .input-group-addon{color:#fff;background-color:#d9534f}.has-feedback label~.form-control-feedback{top:26px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#597ea2}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{padding-top:11px;margin-bottom:0;text-align:right}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:11px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:32px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:19px;font-size:19px}.form-horizontal .form-group-sm .control-label{padding-top:7px;font-size:13px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:10px 15px;font-size:15px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#fff;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#fff;background-color:#95a5a6;border-color:#95a5a6}.btn-default.focus,.btn-default:focus{color:#fff;background-color:#798d8f;border-color:#566566}.btn-default:hover{color:#fff;background-color:#798d8f;border-color:#74898a}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#fff;background-color:#798d8f;background-image:none;border-color:#74898a}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#fff;background-color:#687b7c;border-color:#566566}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#95a5a6;border-color:#95a5a6}.btn-default .badge{color:#95a5a6;background-color:#fff}.btn-primary{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#1a242f;border-color:#000}.btn-primary:hover{color:#fff;background-color:#1a242f;border-color:#161f29}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#1a242f;background-image:none;border-color:#161f29}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#0d1318;border-color:#000}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#2C3E50;border-color:#2C3E50}.btn-primary .badge{color:#2C3E50;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#2d672d}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#2d672d}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#5cb85c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1f7e9a}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1f7e9a}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#5bc0de}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#b06d0f}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#b06d0f}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#8b211e}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#8b211e}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d9534f}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#5cb85c;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#5cb85c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#b4bcc2;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:15px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu-right,.dropdown-menu.pull-right{right:0;left:auto}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{clear:both;font-weight:400;color:#7b8a8b}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#fff;text-decoration:none;background-color:#2C3E50}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#2C3E50;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#b4bcc2}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:13px;color:#b4bcc2}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:66px;padding:18px 27px;font-size:19px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:66px;line-height:66px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:35px;padding:6px 9px;font-size:13px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:35px;line-height:35px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:10px 15px;font-size:15px;font-weight:400;line-height:1;color:#2C3E50;text-align:center;background-color:#ecf0f1;border:1px solid #dce4ec;border-radius:4px}.input-group-addon.input-sm{padding:6px 9px;font-size:13px;border-radius:3px}.input-group-addon.input-lg{padding:18px 27px;font-size:19px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#ecf0f1}.nav>li.disabled>a{color:#b4bcc2}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#b4bcc2;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#ecf0f1}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ecf0f1}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#ecf0f1}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#2C3E50;cursor:default;background-color:#fff;border:1px solid #ecf0f1;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ecf0f1}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ecf0f1;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#2C3E50}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ecf0f1}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ecf0f1;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:60px;margin-bottom:21px;border:1px solid transparent}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.carousel-inner,.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-brand{float:left;height:60px;padding:19.5px 15px;font-size:19px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:13px;margin-bottom:13px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:9.75px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:19.5px;padding-bottom:19.5px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:7.5px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:7.5px;margin-bottom:7.5px}.navbar-btn.btn-sm{margin-top:12.5px;margin-bottom:12.5px}.navbar-btn.btn-xs{margin-top:19px;margin-bottom:19px}.navbar-text{margin-top:19.5px;margin-bottom:19.5px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#2C3E50;border-color:transparent}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#fff}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#fff;background-color:#1a242f}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#fff;background-color:#1a242f}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#5cb85c;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#1a242f}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#1a242f}.navbar-default .navbar-collapse,.navbar-default .navbar-form,.navbar-inverse{border-color:transparent}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#1a242f}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-default .navbar-link{color:#fff}.navbar-default .navbar-link:hover{color:#5cb85c}.navbar-default .btn-link{color:#fff}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#5cb85c}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#5cb85c}.navbar-inverse .navbar-brand{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#2C3E50;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#fff}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#2C3E50;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#4cae4c}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#4cae4c}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#fff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#2C3E50;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#4cae4c}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#449d44}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#449d44}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#49a749}.navbar-inverse .navbar-link{color:#fff}.navbar-inverse .navbar-link:hover{color:#2C3E50}.navbar-inverse .btn-link{color:#fff}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#2C3E50}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#ccc}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#ecf0f1;border-radius:4px}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#95a5a6}.pagination{padding-left:0;margin:21px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:10px 15px;margin-left:-1px;line-height:1.42857143;color:#fff;text-decoration:none;background-color:#5cb85c;border:1px solid transparent}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#fff;background-color:#3d8b3d;border-color:transparent}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#3d8b3d;border-color:transparent}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#ecf0f1;cursor:not-allowed;background-color:#91cf91;border-color:transparent}.pagination-lg>li>a,.pagination-lg>li>span{padding:18px 27px;font-size:19px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:6px 9px;font-size:13px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:21px 0;text-align:center;list-style:none}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#5cb85c;border:1px solid transparent;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#3d8b3d}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#fff;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;color:#fff;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#95a5a6}.label-default[href]:focus,.label-default[href]:hover{background-color:#798d8f}.label-primary{background-color:#2C3E50}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#1a242f}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:13px;color:#fff;vertical-align:middle;background-color:#2C3E50;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#2C3E50;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#ecf0f1}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.alert,.thumbnail{margin-bottom:21px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#cfd9db}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:68px}}.thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ecf0f1;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#5cb85c}.thumbnail .caption{padding:9px;color:#2C3E50}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{top:0;right:0;bottom:0;left:0}.alert-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.alert-success hr{border-top-color:#4cae4c}.alert-success .alert-link{color:#e6e6e6}.alert-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.alert-info hr{border-top-color:#46b8da}.alert-info .alert-link{color:#e6e6e6}.alert-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.alert-warning hr{border-top-color:#eea236}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.alert-danger hr{border-top-color:#d43f3a}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{margin-bottom:21px;background-color:#ecf0f1;border-radius:4px}.progress-bar{float:left;width:0%;height:100%;font-size:13px;line-height:21px;color:#fff;text-align:center;background-color:#2C3E50;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ecf0f1}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#b4bcc2;cursor:not-allowed;background-color:#ecf0f1}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#b4bcc2}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#2C3E50;border-color:#2C3E50}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#8aa4be}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#ecf0f1}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#fff;background-color:#5cb85c}a.list-group-item-success,button.list-group-item-success{color:#fff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#fff;background-color:#4cae4c}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-info{color:#fff;background-color:#5bc0de}a.list-group-item-info,button.list-group-item-info{color:#fff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#fff;background-color:#46b8da}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-warning{color:#fff;background-color:#f0ad4e}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#fff;background-color:#eea236}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#fff;background-color:#d9534f}a.list-group-item-danger,button.list-group-item-danger{color:#fff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#fff;background-color:#d43f3a}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#fff;border-color:#fff}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-title{margin-top:0;font-size:17px}.panel-footer{padding:10px 15px;background-color:#ecf0f1;border-top:1px solid #ecf0f1;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ecf0f1}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ecf0f1}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ecf0f1}.panel-default{border-color:#ecf0f1}.panel-default>.panel-heading{color:#2C3E50;background-color:#ecf0f1;border-color:#ecf0f1}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ecf0f1}.panel-default>.panel-heading .badge{color:#ecf0f1;background-color:#2C3E50}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ecf0f1}.panel-primary{border-color:#2C3E50}.panel-primary>.panel-heading{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#2C3E50}.panel-primary>.panel-heading .badge{color:#2C3E50;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#2C3E50}.panel-success{border-color:#5cb85c}.panel-success>.panel-heading{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#5cb85c}.panel-success>.panel-heading .badge{color:#5cb85c;background-color:#fff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#5cb85c}.panel-info{border-color:#5bc0de}.panel-info>.panel-heading{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#5bc0de}.panel-info>.panel-heading .badge{color:#5bc0de;background-color:#fff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#5bc0de}.panel-warning{border-color:#f0ad4e}.panel-warning>.panel-heading{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0ad4e}.panel-warning>.panel-heading .badge{color:#f0ad4e;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0ad4e}.panel-danger{border-color:#d9534f}.panel-danger>.panel-heading{color:#fff;background-color:#d9534f;border-color:#d9534f}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d9534f}.panel-danger>.panel-heading .badge{color:#d9534f;background-color:#fff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d9534f}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#ecf0f1;border:1px solid transparent;border-radius:4px}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:22.5px;line-height:1;text-shadow:none;filter:alpha(opacity=20)}.popover,.tooltip{font-family:Lato,"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-break:auto;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;text-decoration:none}.modal-title,.popover,.tooltip{line-height:1.42857143}.close:focus,.close:hover{text-decoration:none;cursor:pointer;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{position:fixed;z-index:1050;display:none;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:13px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:15px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.left>.arrow:after,.popover.right>.arrow:after{bottom:-10px;content:" "}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:15px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel-inner{width:100%}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.navbar{border-width:0}.navbar-default .badge{background-color:#fff;color:#2C3E50}.navbar-inverse .badge{background-color:#fff;color:#5cb85c}.navbar-brand{line-height:1}.btn{border-width:2px}.btn-group.open .dropdown-toggle,.btn:active{-webkit-box-shadow:none;box-shadow:none}.text-primary,.text-primary:hover{color:#2C3E50}.text-success,.text-success:hover{color:#5cb85c}.text-danger,.text-danger:hover{color:#d9534f}.text-warning,.text-warning:hover{color:#f0ad4e}.text-info,.text-info:hover{color:#5bc0de}.table a:not(.btn),table a:not(.btn){text-decoration:underline}.close,.table .dropdown-menu a,table .dropdown-menu a{text-decoration:none}.table .danger,.table .danger>a,.table .danger>td>a,.table .danger>th>a,.table .info,.table .info>a,.table .info>td>a,.table .info>th>a,.table .success,.table .success>a,.table .success>td>a,.table .success>th>a,.table .warning,.table .warning>a,.table .warning>td>a,.table .warning>th>a,table .danger,table .danger>a,table .danger>td>a,table .danger>th>a,table .info,table .info>a,table .info>td>a,table .info>th>a,table .success,table .success>a,table .success>td>a,table .success>th>a,table .warning,table .warning>a,table .warning>td>a,table .warning>th>a{color:#fff}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th,table>tbody>tr>td,table>tbody>tr>th,table>tfoot>tr>td,table>tfoot>tr>th,table>thead>tr>td,table>thead>tr>th{border:none}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th,table-bordered>tbody>tr>td,table-bordered>tbody>tr>th,table-bordered>tfoot>tr>td,table-bordered>tfoot>tr>th,table-bordered>thead>tr>td,table-bordered>thead>tr>th{border:1px solid #ecf0f1}.form-control,input{border-width:2px;-webkit-box-shadow:none;box-shadow:none}.form-control:focus,input:focus{-webkit-box-shadow:none;box-shadow:none}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#f0ad4e}.has-warning .form-control,.has-warning .form-control:focus{border:2px solid #f0ad4e}.has-warning .input-group-addon{border-color:#f0ad4e}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#d9534f}.has-error .form-control,.has-error .form-control:focus{border:2px solid #d9534f}.has-error .input-group-addon{border-color:#d9534f}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#5cb85c}.has-success .form-control,.has-success .form-control:focus{border:2px solid #5cb85c}.has-success .input-group-addon{border-color:#5cb85c}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{border-color:transparent}.pager a,.pager a:hover{color:#fff}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{background-color:#91cf91}.close{color:#fff;opacity:.4}.close:focus,.close:hover{color:#fff;opacity:1}.alert .alert-link{color:#fff;text-decoration:underline}.progress{height:10px;-webkit-box-shadow:none;box-shadow:none}.progress .progress-bar{font-size:10px;line-height:10px}.well{-webkit-box-shadow:none;box-shadow:none}a.list-group-item.active,a.list-group-item.active:focus,a.list-group-item.active:hover{border-color:#ecf0f1}a.list-group-item-success.active{background-color:#5cb85c}a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{background-color:#4cae4c}a.list-group-item-warning.active{background-color:#f0ad4e}a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{background-color:#eea236}a.list-group-item-danger.active{background-color:#d9534f}a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{background-color:#d43f3a}.modal .close,.panel-default .close,.popover{color:#2C3E50}@font-face{font-family:'Glyphicons Halflings';src:url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot);src:url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff) format('woff'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:rgba(236,240,241,.4)}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:rgba(221,228,230,.4)}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:rgba(92,184,92,.4)}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:rgba(76,174,76,.4)}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:rgba(91,192,222,.4)}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:rgba(70,184,218,.4)}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:rgba(240,173,78,.4)}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:rgba(238,162,54,.4)}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:rgba(217,83,79,.4)}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:rgba(212,63,58,.4)}.row.vertical-flexbox-row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;flex-wrap:wrap}.row.vertical-flexbox-row>[class*=col-]{display:flex;flex-direction:column}.nav-pills>li>a,.nav-tabs>li>a{color:#000}.table .active,.table .danger,.table .info,.table .success,.table .warning{color:#222}.table .active>td>a:not(.btn),.table .danger>td>a:not(.btn),.table .info>td>a:not(.btn),.table .success>td>a:not(.btn),.table .warning>td>a:not(.btn){color:#5cb85c} \ No newline at end of file diff --git a/allianceauth/templates/allianceauth/base.html b/allianceauth/templates/allianceauth/base.html deleted file mode 100644 index 6a62d8ba..00000000 --- a/allianceauth/templates/allianceauth/base.html +++ /dev/null @@ -1,64 +0,0 @@ -{% load i18n %} -{% load navactive %} -{% load auth_notifications %} - - - - - - - - - {% include 'allianceauth/icons.html' %} - - {% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %} - - {% include 'bundles/bootstrap-css.html' %} - {% include 'bundles/fontawesome.html' %} - {% include 'bundles/auth-base-css.html' %} - - {% block extra_css %}{% endblock extra_css %} - - - -
- - {% include 'allianceauth/top-menu.html' %} - -
- {% if user.is_authenticated %} - {% include 'allianceauth/side-menu.html' %} - {% endif %} - -
- {% include 'allianceauth/messages.html' %} - - {% block content %} - {% endblock content %} -
-
-
- - {% include 'bundles/bootstrap-js.html' %} - {% include 'bundles/jquery-visibility-js.html' %} - - {% if user.is_authenticated %} - - {% include 'bundles/refresh-notifications-js.html' %} - {% endif %} - {% include 'bundles/evetime-js.html' %} - - {% block extra_javascript %} - {% endblock extra_javascript %} - - - diff --git a/allianceauth/templates/allianceauth/messages.html b/allianceauth/templates/allianceauth/messages.html deleted file mode 100644 index 8303a33c..00000000 --- a/allianceauth/templates/allianceauth/messages.html +++ /dev/null @@ -1,27 +0,0 @@ -{% load i18n %} - -{% if messages %} - {% for message in messages %} - - {% endfor %} -{% endif %} diff --git a/allianceauth/templates/allianceauth/night-toggle.html b/allianceauth/templates/allianceauth/night-toggle.html deleted file mode 100644 index 8a58c8b0..00000000 --- a/allianceauth/templates/allianceauth/night-toggle.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load i18n %} - -
  • - - - {% translate "Night Mode" %} - -
  • diff --git a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html b/allianceauth/templates/allianceauth/top-menu-user-dropdown.html deleted file mode 100644 index a9478e21..00000000 --- a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html +++ /dev/null @@ -1,72 +0,0 @@ -{% load i18n %} -{% load evelinks %} - - diff --git a/allianceauth/templates/allianceauth/top-menu.html b/allianceauth/templates/allianceauth/top-menu.html deleted file mode 100644 index 60914386..00000000 --- a/allianceauth/templates/allianceauth/top-menu.html +++ /dev/null @@ -1,36 +0,0 @@ -{% load i18n %} -{% load static %} -{% load navactive %} - diff --git a/allianceauth/templates/bundles/bootstrap-css.html b/allianceauth/templates/bundles/bootstrap-css.html deleted file mode 100644 index bed760ba..00000000 --- a/allianceauth/templates/bundles/bootstrap-css.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load static %} - -{% if NIGHT_MODE %} - {% if debug %} - - - - {% else %} - - {% endif %} -{% else %} - {% if debug %} - - - - {% else %} - - {% endif %} -{% endif %} - diff --git a/allianceauth/templates/bundles/bootstrap-js.html b/allianceauth/templates/bundles/bootstrap-js.html deleted file mode 100644 index 6447ab7e..00000000 --- a/allianceauth/templates/bundles/bootstrap-js.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/allianceauth/templates/bundles/datatables-css.html b/allianceauth/templates/bundles/datatables-css.html deleted file mode 100644 index f43dccc0..00000000 --- a/allianceauth/templates/bundles/datatables-css.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/allianceauth/templates/bundles/datatables-js.html b/allianceauth/templates/bundles/datatables-js.html deleted file mode 100644 index f6afbf86..00000000 --- a/allianceauth/templates/bundles/datatables-js.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/customizing/index.md b/docs/customizing/index.md index 38ccada9..db95dbf4 100644 --- a/docs/customizing/index.md +++ b/docs/customizing/index.md @@ -22,7 +22,7 @@ Within your auth project exists two folders named `static` and `templates`. Thes You can add extra static or templates by putting files in these folders. Note that changes to static require running the `python manage.py collectstatic` command to copy to the web server directory. -It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base-bs5.html` (Bootstrap 5) `allianceauth/templates/allianceauth/base.html` (Legacy BS3) file to `myauth/templates/allianceauth/*.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included. +It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base-bs5.html` file to `myauth/templates/allianceauth/*.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included. ## Custom URLs and Views From c6890dd2c61fd20ecf1ebc585c626f5b272ed0de Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 18:01:01 +1000 Subject: [PATCH 028/178] Spread esi tasks over 10 minutes --- allianceauth/eveonline/tasks.py | 42 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/allianceauth/eveonline/tasks.py b/allianceauth/eveonline/tasks.py index ce2b4219..657e63f1 100644 --- a/allianceauth/eveonline/tasks.py +++ b/allianceauth/eveonline/tasks.py @@ -1,4 +1,5 @@ import logging +from random import randint from celery import shared_task @@ -9,7 +10,8 @@ from . import providers logger = logging.getLogger(__name__) TASK_PRIORITY = 7 -CHUNK_SIZE = 500 +CHARACTER_AFFILIATION_CHUNK_SIZE = 500 +EVEONLINE_TASK_JITTER = 600 def chunks(lst, n): @@ -19,13 +21,13 @@ def chunks(lst, n): @shared_task -def update_corp(corp_id): +def update_corp(corp_id: int) -> None: """Update given corporation from ESI""" EveCorporationInfo.objects.update_corporation(corp_id) @shared_task -def update_alliance(alliance_id): +def update_alliance(alliance_id: int) -> None: """Update given alliance from ESI""" EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance() @@ -37,23 +39,30 @@ def update_character(character_id: int) -> None: @shared_task -def run_model_update(): +def run_model_update() -> None: """Update all alliances, corporations and characters from ESI""" - #update existing corp models + # Queue update tasks for Known Corporation Models for corp in EveCorporationInfo.objects.all().values('corporation_id'): - update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY) + update_corp.apply_async( + args=[corp['corporation_id']], + priority=TASK_PRIORITY, + countdown=randint(1, EVEONLINE_TASK_JITTER)) - # update existing alliance models + # Queue update tasks for Known Alliance Models for alliance in EveAllianceInfo.objects.all().values('alliance_id'): - update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY) + update_alliance.apply_async( + args=[alliance['alliance_id']], + priority=TASK_PRIORITY, + countdown=randint(1, EVEONLINE_TASK_JITTER)) - # update existing character models + # Queue update tasks for Known Character Models character_ids = EveCharacter.objects.all().values_list('character_id', flat=True) - for character_ids_chunk in chunks(character_ids, CHUNK_SIZE): + for character_ids_chunk in chunks(character_ids, CHARACTER_AFFILIATION_CHUNK_SIZE): update_character_chunk.apply_async( - args=[character_ids_chunk], priority=TASK_PRIORITY - ) + args=[character_ids_chunk], + priority=TASK_PRIORITY, + countdown=randint(1, EVEONLINE_TASK_JITTER)) @shared_task @@ -68,8 +77,9 @@ def update_character_chunk(character_ids_chunk: list): logger.info("Failed to bulk update characters. Attempting single updates") for character_id in character_ids_chunk: update_character.apply_async( - args=[character_id], priority=TASK_PRIORITY - ) + args=[character_id], + priority=TASK_PRIORITY, + countdown=randint(1, EVEONLINE_TASK_JITTER)) return affiliations = { @@ -107,5 +117,5 @@ def update_character_chunk(character_ids_chunk: list): if corp_changed or alliance_changed or name_changed: update_character.apply_async( - args=[character.get('character_id')], priority=TASK_PRIORITY - ) + args=[character.get('character_id')], + priority=TASK_PRIORITY) From 292fb7b29d152e44a715047a07266703be5a2576 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 18:35:07 +1000 Subject: [PATCH 029/178] Add docs for smoothing out task execution --- docs/development/tech_docu/celery.md | 45 +++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/development/tech_docu/celery.md b/docs/development/tech_docu/celery.md index 537b7d3d..483894bb 100644 --- a/docs/development/tech_docu/celery.md +++ b/docs/development/tech_docu/celery.md @@ -168,6 +168,49 @@ example.apply_async(priority=3) For defining a priority to tasks, you cannot use the convenient shortcut ``delay()``, but instead need to start a task with ``apply_async()``, which also requires you to pass parameters to your task function differently. Please check out the `official docs `_ for details. ::: +## Rate-Limiting and Smoothing of Task Execution + +Large numbers of installs running the same crontab (ie. `0 * * * *`) can all slam an external service at the same time. + +Consider Artificially smoothing out your tasks with a few methods + +### Offset Crontabs + +Avoid running your tasks on the hour or other nice neat human numbers, consider 23 minutes on the hour instead of at zero (`28 * * * *`) + +### Subset Tasks + +Slice your tasks needed up into more manageable chunks and run them more often. 1/10th of your tasks run 10x more often will return the same end result with less peak loads on external services and your task queue. + +### Celery ETA/Countdown + +Scatter your tasks across a larger window using + +This example will queue up tasks across the next 10 minutes, trickling them into your workers (and the external service) + +```python +for corp in EveCorporationInfo.objects.all().values('corporation_id'): + update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY) + update_corp.apply_async( + args=[corp['corporation_id']], + priority=TASK_PRIORITY, + countdown=randint(1, 600)) +``` + +### Celery Rate Limits + +Celery Rate Limits come with a small catch, its _per worker_, you may have to be either very conservative or have these configurable by the end user if they varied their worker count. + + + +This example of 10 Tasks per Minute will result in ~100 tasks per minute at 10 Workers + +```python +@shared_task(rate_limit="10/m") +def update_charactercorporationhistory(character_id: int) -> None: + """Update CharacterCorporationHistory models from ESI""" +``` + ## What special features should I be aware of? Every Alliance Auth installation will come with a couple of special celery related features "out-of-the-box" that you can make use of in your apps. @@ -192,6 +235,6 @@ You can use it like so: Please see the [official documentation](https://pypi.org/project/celery_once/) of celery-once for details. -### task priorities +### Task Priorities Alliance Auth is using task priorities to enable priority-based scheduling of task execution. Please see [How can I use priorities for tasks?](#how-can-i-use-priorities-for-tasks) for details. From b95f393a4cc63602daaccd37e3d285dce705ce62 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 21:31:16 +1000 Subject: [PATCH 030/178] update pre-commit --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc329454..b8374404 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,20 +5,20 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.4 + rev: v0.8.1 hooks: # Run the linter, and only the linter - id: ruff - repo: https://github.com/adamchainz/django-upgrade - rev: 1.21.0 + rev: 1.22.2 hooks: - id: django-upgrade args: [--target-version=4.2] # Formatting - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: # Identify invalid files - id: check-ast @@ -75,14 +75,14 @@ repos: swagger\.json ) - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.41.0 + rev: v0.43.0 hooks: - id: markdownlint args: - --disable=MD013 # Infrastructure - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.3 + rev: v2.5.0 hooks: - id: pyproject-fmt name: pyproject.toml formatter @@ -92,7 +92,7 @@ repos: additional_dependencies: - tox==4.18.1 # https://github.com/tox-dev/tox/releases/latest - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.23 hooks: - id: validate-pyproject name: Validate pyproject.toml From 8f4daea14f9f55d92911fae8f755b34cd905fbbd Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 21:32:28 +1000 Subject: [PATCH 031/178] sphinx theme v3 is now stable --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 65061af6..68c8c658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ optional-dependencies.docs = [ "myst-parser>=4", "sphinx>=8", "sphinx-copybutton", - "sphinx-rtd-theme<4,>=3.0.0rc1", + "sphinx-rtd-theme>=3,<4", "sphinx-tabs", "sphinxcontrib-django", ] From f4c024d199a1520669e83dab2a0bb43a2acb285c Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 21:32:54 +1000 Subject: [PATCH 032/178] pre-commit fixes --- pyproject.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 68c8c658..001c00c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "flit_core.buildapi" requires = [ - "flit-core<4,>=3.2", + "flit-core>=3.2,<4", ] [project] @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] @@ -39,15 +40,15 @@ dynamic = [ dependencies = [ "bcrypt", "beautifulsoup4", - "celery<6,>=5.4", + "celery>=5.4,<6", "celery-once>=3.0.1", - "django<5.2,>=5.1", + "django>=5.1,<5.2", "django-bootstrap-form", "django-bootstrap5>=23.3", "django-celery-beat>=2.7", "django-esi>=5", "django-redis>=5.4", - "django-registration<4,>=3.3", + "django-registration>=5.1,<6", "django-solo", "django-sortedm2m", "dnspython", From eaba01ad97b137182ba981129971ad46f09e6123 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 21:33:20 +1000 Subject: [PATCH 033/178] remove checks that have expired and bump others --- allianceauth/checks.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index e6d0319d..4a994039 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -43,11 +43,12 @@ def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: except InvalidVersion: errors.append(Warning("Unable to confirm Redis Version")) return errors - - if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=datetime.timezone.utc): + if redis_version.major == 7 and redis_version.minor == 4 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=datetime.timezone.utc): errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001")) + elif redis_version.major == 7 and redis_version.minor == 2: + errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A019")) elif redis_version.major == 7 and redis_version.minor == 0: - errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A002")) + errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A019")) elif redis_version.major == 6 and redis_version.minor == 2: errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A018")) elif redis_version.major in [6, 5]: @@ -71,11 +72,7 @@ def system_package_mysql(app_configs, **kwargs) -> list[CheckMessage]: # MySQL 8 if mysql_version.major == 8 and mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=datetime.timezone.utc): errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A004")) - elif mysql_version.major == 8 and mysql_version.minor == 3: - errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A005")) - elif mysql_version.major == 8 and mysql_version.minor == 2: - errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A006")) - elif mysql_version.major == 8 and mysql_version.minor == 1: + elif mysql_version.major == 8 and mysql_version.minor in [1, 2, 3]: errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A007")) elif mysql_version.major == 8 and mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=datetime.timezone.utc): errors.append(Error(f"MySQL {mysql_version.public} EOL", hint="https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/", id="allianceauth.checks.A008")) @@ -99,14 +96,7 @@ def system_package_mariadb(app_configs, **kwargs) -> list[CheckMessage]: # MariaDB 11 if mariadb_version.major == 11 and mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=datetime.timezone.utc): errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A010")) - elif mariadb_version.major == 11 and mariadb_version.minor == 2: - errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A018")) - if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=datetime.timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A011")) - elif mariadb_version.major == 11 and mariadb_version.minor == 1: - errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A019")) - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config", id="allianceauth.checks.A012")) - elif mariadb_version.major == 11 and mariadb_version.minor in [0, 3]: # Demote versions down here once EOL + elif mariadb_version.major == 11 and mariadb_version.minor in [0, 1, 2, 3, 5]: # Demote versions down here once EOL errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint="https://mariadb.org/download/?t=repo-config.", id="allianceauth.checks.A013")) # MariaDB 10 From dc0c1a2818ba9ca11d057f7c1087332da874748a Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 21:33:48 +1000 Subject: [PATCH 034/178] collapse docker compose to make discord less unhappy --- docker/docker-compose.yml | 66 +++++---------------------------------- 1 file changed, 7 insertions(+), 59 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 479422e8..c9eca829 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -28,11 +28,7 @@ x-allianceauth-base: &allianceauth-base x-allianceauth-health-check: &allianceauth-health-checks healthcheck: - test: [ - "CMD", - "/memory_check.sh", - "500000000" - ] + test: ["CMD", "/memory_check.sh", "500000000"] interval: 60s timeout: 10s retries: 3 @@ -89,47 +85,25 @@ services: max-file: "5" allianceauth_gunicorn: - ports: - - 8000:8000 container_name: allianceauth_gunicorn <<: [*allianceauth-base] - entrypoint: [ - "gunicorn", - "myauth.wsgi", - "--bind=0.0.0.0:8000", - "--workers=3", - "--timeout=120", - "--max-requests=500", - "--max-requests-jitter=50" - ] + entrypoint: ["gunicorn", "myauth.wsgi", "--bind=0.0.0.0:8000", "--workers=3", "--timeout=120", "--max-requests=500", "--max-requests-jitter=50"] + ports: + - 8000:8000 allianceauth_beat: container_name: allianceauth_worker_beat <<: [*allianceauth-base] - entrypoint: [ - "celery", - "-A", - "myauth", - "beat" - ] + entrypoint: ["celery", "-A", "myauth", "beat"] allianceauth_worker: <<: [*allianceauth-base, *allianceauth-health-checks] - entrypoint: [ - "celery", - "-A", - "myauth", - "worker", - "--pool=threads", - "--concurrency=5", - "-n", - "worker_%n" - ] + entrypoint: ["celery", "-A", "myauth", "worker", "--pool=threads", "--concurrency=5", "-n", "worker_%n"] deploy: replicas: 2 grafana: - image: grafana/grafana-oss:11.2.0 + image: grafana/grafana-oss:latest restart: always depends_on: - auth_mysql @@ -156,13 +130,6 @@ services: - ${PROXY_HTTP_PORT:-80}:80 - ${PROXY_DASH_PORT:-81}:81 - ${PROXY_HTTPS_PORT:-443}:443 - # Uncomment this section to use a dedicated database for Nginx Proxy Manager - # environment: - # DB_MYSQL_HOST: "proxy-db" - # DB_MYSQL_PORT: 3306 - # DB_MYSQL_USER: "npm" - # DB_MYSQL_PASSWORD: "${PROXY_MYSQL_PASS?err}" - # DB_MYSQL_NAME: "npm" volumes: - proxy-data:/data - proxy-le:/etc/letsencrypt @@ -174,25 +141,6 @@ services: max-size: "10Mb" max-file: "5" - # Uncomment this section to use a dedicated database for Nginx Proxy Manager - # proxy-db: - # image: 'jc21/mariadb-aria:latest' - # restart: always - # environment: - # MYSQL_ROOT_PASSWORD: "${PROXY_MYSQL_PASS_ROOT?err}" - # MYSQL_DATABASE: 'npm' - # MYSQL_USER: 'npm' - # MYSQL_PASSWORD: "${PROXY_MYSQL_PASS?err}" - # ports: - # - 3306 - # volumes: - # - proxy-db:/var/lib/mysql - # logging: - # driver: "json-file" - # options: - # max-size: "1Mb" - # max-file: "5" - volumes: redis-data: static-volume: From 5e836c428562b0473c2a52c5ae3a6eb8e0a555ff Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 22:08:47 +1000 Subject: [PATCH 035/178] pre-commit fixes --- .../migrations/0002_migrate_permissions.py | 2 +- allianceauth/corputils/views.py | 2 +- allianceauth/eveonline/admin.py | 16 ++++++------ allianceauth/eveonline/providers.py | 20 +++++++-------- .../eveonline/tests/esi_client_stub.py | 12 ++++----- allianceauth/fleetactivitytracking/views.py | 14 ++++++++--- allianceauth/groupmanagement/views.py | 8 +++--- allianceauth/hrapplications/views.py | 4 +-- allianceauth/menu/models.py | 2 +- .../menu/templatetags/menu_menu_items.py | 2 +- allianceauth/permissions_tool/views.py | 4 +-- allianceauth/services/abstract.py | 5 ++-- .../tests/piloting_concurrency.py | 14 +++++------ .../services/modules/discord/tasks.py | 20 +++++++-------- .../services/modules/discourse/tasks.py | 2 +- allianceauth/services/modules/mumble/tasks.py | 8 +++--- .../services/modules/openfire/tasks.py | 4 +-- .../services/modules/phpbb3/models.py | 6 ++--- allianceauth/services/modules/phpbb3/tasks.py | 4 +-- allianceauth/services/modules/smf/tasks.py | 8 +++--- .../services/modules/teamspeak3/tasks.py | 2 +- .../services/modules/teamspeak3/util/ts3.py | 4 +-- allianceauth/services/signals.py | 25 ++++++++++--------- 23 files changed, 97 insertions(+), 91 deletions(-) diff --git a/allianceauth/corputils/migrations/0002_migrate_permissions.py b/allianceauth/corputils/migrations/0002_migrate_permissions.py index cd75c9dc..0d0db9aa 100644 --- a/allianceauth/corputils/migrations/0002_migrate_permissions.py +++ b/allianceauth/corputils/migrations/0002_migrate_permissions.py @@ -70,7 +70,7 @@ def forward(apps, schema_editor): perm.delete() -def reverse(apps, schema_editor): +def reverse(apps, schema_editor): # noqa: C901 perm_dict = user_permissions_dict(apps) corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats']) diff --git a/allianceauth/corputils/views.py b/allianceauth/corputils/views.py index d0e62690..67d846ea 100644 --- a/allianceauth/corputils/views.py +++ b/allianceauth/corputils/views.py @@ -62,7 +62,7 @@ def corpstats_add(request, token): @login_required @user_passes_test(access_corpstats_test) -def corpstats_view(request, corp_id=None): +def corpstats_view(request, corp_id=None): # noqa: C901 corpstats = None # get requested model diff --git a/allianceauth/eveonline/admin.py b/allianceauth/eveonline/admin.py index 69d428df..b75b759a 100644 --- a/allianceauth/eveonline/admin.py +++ b/allianceauth/eveonline/admin.py @@ -49,8 +49,8 @@ class EveFactionForm(EveEntityForm): def clean_id(self): try: assert self.Meta.model.provider.get_faction(self.cleaned_data['id']) - except (AssertionError, ObjectNotFound): - raise EveEntityNotFoundError('faction', self.cleaned_data['id']) + except (AssertionError, ObjectNotFound) as e: + raise EveEntityNotFoundError('faction', self.cleaned_data['id']) from e if self.Meta.model.objects.filter(faction_id=self.cleaned_data['id']).exists(): raise EveEntityExistsError('faction', self.cleaned_data['id']) return self.cleaned_data['id'] @@ -70,8 +70,8 @@ class EveCharacterForm(EveEntityForm): def clean_id(self): try: assert self.Meta.model.provider.get_character(self.cleaned_data['id']) - except (AssertionError, ObjectNotFound): - raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) + except (AssertionError, ObjectNotFound) as e: + raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e if self.Meta.model.objects.filter(character_id=self.cleaned_data['id']).exists(): raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) return self.cleaned_data['id'] @@ -90,8 +90,8 @@ class EveCorporationForm(EveEntityForm): def clean_id(self): try: assert self.Meta.model.provider.get_corporation(self.cleaned_data['id']) - except (AssertionError, ObjectNotFound): - raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) + except (AssertionError, ObjectNotFound) as e: + raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e if self.Meta.model.objects.filter(corporation_id=self.cleaned_data['id']).exists(): raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) return self.cleaned_data['id'] @@ -110,8 +110,8 @@ class EveAllianceForm(EveEntityForm): def clean_id(self): try: assert self.Meta.model.provider.get_alliance(self.cleaned_data['id']) - except (AssertionError, ObjectNotFound): - raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) + except (AssertionError, ObjectNotFound) as e: + raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id']) from e if self.Meta.model.objects.filter(alliance_id=self.cleaned_data['id']).exists(): raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id']) return self.cleaned_data['id'] diff --git a/allianceauth/eveonline/providers.py b/allianceauth/eveonline/providers.py index 9ba2db92..69203aaa 100644 --- a/allianceauth/eveonline/providers.py +++ b/allianceauth/eveonline/providers.py @@ -223,8 +223,8 @@ class EveSwaggerProvider(EveProvider): faction_id=data['faction_id'] if 'faction_id' in data else None, ) return model - except HTTPNotFound: - raise ObjectNotFound(alliance_id, 'alliance') + except HTTPNotFound as e: + raise ObjectNotFound(alliance_id, 'alliance') from e def get_corp(self, corp_id: int) -> Corporation: """Fetch corporation from ESI.""" @@ -240,8 +240,8 @@ class EveSwaggerProvider(EveProvider): faction_id=data['faction_id'] if 'faction_id' in data else None, ) return model - except HTTPNotFound: - raise ObjectNotFound(corp_id, 'corporation') + except HTTPNotFound as e: + raise ObjectNotFound(corp_id, 'corporation') from e def get_character(self, character_id: int) -> Character: """Fetch character from ESI.""" @@ -256,8 +256,8 @@ class EveSwaggerProvider(EveProvider): faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None, ) return model - except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound): - raise ObjectNotFound(character_id, 'character') + except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound) as e: + raise ObjectNotFound(character_id, 'character') from e def _fetch_character_name(self, character_id: int) -> str: """Fetch character name from ESI.""" @@ -288,16 +288,16 @@ class EveSwaggerProvider(EveProvider): return Entity(id=f['faction_id'], name=f['name']) else: raise KeyError() - except (HTTPNotFound, HTTPUnprocessableEntity, KeyError): - raise ObjectNotFound(faction_id, 'faction') + except (HTTPNotFound, HTTPUnprocessableEntity, KeyError) as e: + raise ObjectNotFound(faction_id, 'faction') from e def get_itemtype(self, type_id: int) -> ItemType: """Fetch inventory item from ESI.""" try: data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result() return ItemType(id=type_id, name=data['name']) - except (HTTPNotFound, HTTPUnprocessableEntity): - raise ObjectNotFound(type_id, 'type') + except (HTTPNotFound, HTTPUnprocessableEntity) as e: + raise ObjectNotFound(type_id, 'type') from e provider = EveSwaggerProvider() diff --git a/allianceauth/eveonline/tests/esi_client_stub.py b/allianceauth/eveonline/tests/esi_client_stub.py index 9382355c..6b7c3ee1 100644 --- a/allianceauth/eveonline/tests/esi_client_stub.py +++ b/allianceauth/eveonline/tests/esi_client_stub.py @@ -57,11 +57,11 @@ class EsiClientStub: } try: return BravadoOperationStub(data[int(alliance_id)]) - except KeyError: + except KeyError as e: response = BravadoResponseStub( 404, f"Alliance with ID {alliance_id} not found" ) - raise HTTPNotFound(response) + raise HTTPNotFound(response) from e @staticmethod def get_alliances_alliance_id_corporations(alliance_id): @@ -87,11 +87,11 @@ class EsiClientStub: } try: return BravadoOperationStub(data[int(character_id)]) - except KeyError: + except KeyError as e: response = BravadoResponseStub( 404, f"Character with ID {character_id} not found" ) - raise HTTPNotFound(response) + raise HTTPNotFound(response) from e @staticmethod def post_characters_affiliation(characters: list): @@ -147,11 +147,11 @@ class EsiClientStub: } try: return BravadoOperationStub(data[int(corporation_id)]) - except KeyError: + except KeyError as e: response = BravadoResponseStub( 404, f"Corporation with ID {corporation_id} not found" ) - raise HTTPNotFound(response) + raise HTTPNotFound(response) from e class Universe: @staticmethod diff --git a/allianceauth/fleetactivitytracking/views.py b/allianceauth/fleetactivitytracking/views.py index d7c3ee07..5778ce9a 100644 --- a/allianceauth/fleetactivitytracking/views.py +++ b/allianceauth/fleetactivitytracking/views.py @@ -143,7 +143,12 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None): @login_required @permission_required('auth.fleetactivitytracking_statistics') -def fatlink_statistics_view(request, year=datetime.date.today().year, month=datetime.date.today().month): +def fatlink_statistics_view(request, year=None, month=None): + if year is None: + year = datetime.date.today().year + if month is None: + month = datetime.date.today().month + year = int(year) month = int(month) start_of_month = datetime.datetime(year, month, 1) @@ -176,9 +181,12 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date @login_required -def fatlink_personal_statistics_view(request, year=datetime.date.today().year): +def fatlink_personal_statistics_view(request, year=None): + if year is None: + year = datetime.date.today().year + year = int(year) - logger.debug("Personal statistics view for year %i called by %s" % (year, request.user)) + logger.debug(f"Personal statistics view for year {year} called by {request.user}") user = request.user logger.debug(f"fatlink_personal_statistics_view called by user {request.user}") diff --git a/allianceauth/groupmanagement/views.py b/allianceauth/groupmanagement/views.py index 1c736573..feab4c91 100644 --- a/allianceauth/groupmanagement/views.py +++ b/allianceauth/groupmanagement/views.py @@ -87,8 +87,8 @@ def group_membership_audit(request, group_id): logger.warning(f"User {request.user} attempted to view the membership of group {group_id} but permission was denied") raise PermissionDenied - except ObjectDoesNotExist: - raise Http404("Group does not exist") + except ObjectDoesNotExist as e: + raise Http404("Group does not exist") from e render_items = {'group': group} entries = RequestLog.objects.filter(group=group).order_by('-date') render_items['entries'] = entries @@ -117,8 +117,8 @@ def group_membership_list(request, group_id): ) raise PermissionDenied - except ObjectDoesNotExist: - raise Http404("Group does not exist") + except ObjectDoesNotExist as e: + raise Http404("Group does not exist") from e group_leaders = group.authgroup.group_leaders.all() members = [] diff --git a/allianceauth/hrapplications/views.py b/allianceauth/hrapplications/views.py index 2102b663..395aac17 100644 --- a/allianceauth/hrapplications/views.py +++ b/allianceauth/hrapplications/views.py @@ -111,8 +111,8 @@ def hr_application_view(request, app_id): logger.debug(f"hr_application_view called by user {request.user} for app id {app_id}") try: app = Application.objects.prefetch_related('responses', 'comments', 'comments__user').get(pk=app_id) - except Application.DoesNotExist: - raise Http404 + except Application.DoesNotExist as e: + raise Http404 from e if request.method == 'POST': if request.user.has_perm('hrapplications.add_applicationcomment'): form = HRApplicationCommentForm(request.POST) diff --git a/allianceauth/menu/models.py b/allianceauth/menu/models.py index 40c91aab..0e097073 100644 --- a/allianceauth/menu/models.py +++ b/allianceauth/menu/models.py @@ -47,7 +47,7 @@ class MenuItem(models.Model): ) # app related properties - hook_hash = models.CharField( + hook_hash = models.CharField( # noqa: DJ001 max_length=64, default=None, null=True, diff --git a/allianceauth/menu/templatetags/menu_menu_items.py b/allianceauth/menu/templatetags/menu_menu_items.py index 6f93b9fb..d09e2012 100644 --- a/allianceauth/menu/templatetags/menu_menu_items.py +++ b/allianceauth/menu/templatetags/menu_menu_items.py @@ -77,7 +77,7 @@ class RenderedMenuItem: self.html_id = hook_obj.html_id -def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: +def render_menu(request: HttpRequest) -> list[RenderedMenuItem]: # noqa: C901 """Return the rendered side menu for including in a template. This function is creating BS5 style menus. diff --git a/allianceauth/permissions_tool/views.py b/allianceauth/permissions_tool/views.py index de4cc281..62ca95ff 100644 --- a/allianceauth/permissions_tool/views.py +++ b/allianceauth/permissions_tool/views.py @@ -48,8 +48,8 @@ def permissions_audit(request, app_label, model, codename): .prefetch_related('group_set', 'user_set', 'state_set', 'state_set__userprofile_set', 'group_set__user_set', 'state_set__userprofile_set__user')\ .get(content_type__app_label=app_label, content_type__model=model, codename=codename) - except Permission.DoesNotExist: - raise Http404 + except Permission.DoesNotExist as e: + raise Http404 from e context = {'permission': { 'permission': perm, diff --git a/allianceauth/services/abstract.py b/allianceauth/services/abstract.py index 034d1dfc..4230af38 100644 --- a/allianceauth/services/abstract.py +++ b/allianceauth/services/abstract.py @@ -33,7 +33,6 @@ class AbstractServiceModel(models.Model): related_name='%(app_label)s' ) - class Meta: abstract = True @@ -85,8 +84,8 @@ class ServicesCRUDMixin(SingleObjectMixin): try: return queryset.get(user__pk=self.request.user.pk) - except ObjectDoesNotExist: - raise Http404 + except ObjectDoesNotExist as e: + raise Http404 from e class BaseDeactivateServiceAccountView(ServicesCRUDMixin, BaseServiceView, DeleteView): diff --git a/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py b/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py index 8d9b73da..20844347 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py +++ b/allianceauth/services/modules/discord/discord_client/tests/piloting_concurrency.py @@ -44,26 +44,24 @@ MAX_JITTER_PER_RUN_SECS = 1.0 def worker(num: int): """worker function""" - worker_info = 'worker %d' % num - logger.info('%s: started', worker_info) + worker_info = f'worker {num}' + logger.info(f'{worker_info}: started') client = DiscordClient(DISCORD_BOT_TOKEN) try: runs = 0 while runs < NUMBER_OF_RUNS: - run_info = '%s: run %d' % (worker_info, runs + 1) + run_info = f'{worker_info}: run {runs + 1}' my_jitter_secs = random() * MAX_JITTER_PER_RUN_SECS - logger.info('%s - waiting %s secs', run_info, f'{my_jitter_secs:.3f}') + logger.info(f'{run_info} - waiting {my_jitter_secs:.3f} secs') sleep(my_jitter_secs) - logger.info('%s - started', run_info) + logger.info(f'{run_info} - started') try: client.modify_guild_member( DISCORD_GUILD_ID, DISCORD_USER_ID, nick=NICK ) runs += 1 except DiscordApiBackoff as bo: - message = '%s - waiting out API backoff for %d ms' % ( - run_info, bo.retry_after - ) + message = f'{run_info} - waiting out API backoff for {bo.retry_after} ms' logger.info(message) print() print(message) diff --git a/allianceauth/services/modules/discord/tasks.py b/allianceauth/services/modules/discord/tasks.py index 15e6319a..9881aed8 100644 --- a/allianceauth/services/modules/discord/tasks.py +++ b/allianceauth/services/modules/discord/tasks.py @@ -89,12 +89,12 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None bo, bo.retry_after_seconds ) - raise self.retry(countdown=bo.retry_after_seconds) + raise self.retry(exc=bo, countdown=bo.retry_after_seconds) from bo - except AttributeError: - raise ValueError(f'{method} not a valid method for DiscordUser') + except AttributeError as e: + raise ValueError(f'{method} not a valid method for DiscordUser') from e - except (HTTPError, ConnectionError): + except (HTTPError, ConnectionError) as e: logger.warning( '%s failed for user %s, retrying in %d secs', method, @@ -103,7 +103,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None exc_info=True ) if self.request.retries < DISCORD_TASKS_MAX_RETRIES: - raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) + raise self.retry(exc=e, countdown=DISCORD_TASKS_RETRY_PAUSE) from e else: logger.error( '%s failed for user %s after max retries', @@ -192,8 +192,8 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any: try: result = getattr(DiscordUser.objects, method)(**kwargs) - except AttributeError: - raise ValueError(f'{method} not a valid method for DiscordUser.objects') + except AttributeError as e: + raise ValueError(f'{method} not a valid method for DiscordUser.objects') from e except DiscordApiBackoff as bo: logger.info( @@ -202,9 +202,9 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any: bo, bo.retry_after_seconds ) - raise self.retry(countdown=bo.retry_after_seconds) + raise self.retry(exc=bo, countdown=bo.retry_after_seconds) from bo - except (HTTPError, ConnectionError): + except (HTTPError, ConnectionError)as e: logger.warning( '%s failed, retrying in %d secs', method, @@ -212,7 +212,7 @@ def _task_perform_users_action(self, method: str, **kwargs) -> Any: exc_info=True ) if self.request.retries < DISCORD_TASKS_MAX_RETRIES: - raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE) + raise self.retry(exc=e, countdown=DISCORD_TASKS_RETRY_PAUSE) from e else: logger.error('%s failed after max retries', method, exc_info=True) diff --git a/allianceauth/services/modules/discourse/tasks.py b/allianceauth/services/modules/discourse/tasks.py index 48487e1f..08b281e1 100644 --- a/allianceauth/services/modules/discourse/tasks.py +++ b/allianceauth/services/modules/discourse/tasks.py @@ -52,7 +52,7 @@ class DiscourseTasks: except Exception as e: logger.exception(e) logger.warning(f"Discourse group sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e logger.debug(f"Updated user {user} discourse groups.") @staticmethod diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py index 967abd9f..f88ec297 100644 --- a/allianceauth/services/modules/mumble/tasks.py +++ b/allianceauth/services/modules/mumble/tasks.py @@ -41,9 +41,9 @@ class MumbleTasks: return True except MumbleUser.DoesNotExist: logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account") - except Exception: + except Exception as e: logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e else: logger.debug(f"User {user} does not have a mumble account, skipping") return False @@ -61,9 +61,9 @@ class MumbleTasks: return True except MumbleUser.DoesNotExist: logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account") - except Exception: + except Exception as e: logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e else: logger.debug(f"User {user} does not have a mumble account, skipping") return False diff --git a/allianceauth/services/modules/openfire/tasks.py b/allianceauth/services/modules/openfire/tasks.py index 6c246d37..60baa3c2 100644 --- a/allianceauth/services/modules/openfire/tasks.py +++ b/allianceauth/services/modules/openfire/tasks.py @@ -54,9 +54,9 @@ class OpenfireTasks: logger.debug(f"Updating user {user} jabber groups to {groups}") try: OpenfireManager.update_user_groups(user.openfire.username, groups) - except Exception: + except Exception as e: logger.exception(f"Jabber group sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e logger.debug(f"Updated user {user} jabber groups.") else: logger.debug("User does not have an openfire account") diff --git a/allianceauth/services/modules/phpbb3/models.py b/allianceauth/services/modules/phpbb3/models.py index a283d4b4..0d9d1d1c 100644 --- a/allianceauth/services/modules/phpbb3/models.py +++ b/allianceauth/services/modules/phpbb3/models.py @@ -8,10 +8,10 @@ class Phpbb3User(models.Model): related_name='phpbb3') username = models.CharField(max_length=254) - def __str__(self): - return self.username - class Meta: permissions = ( ("access_phpbb3", "Can access the phpBB3 service"), ) + + def __str__(self) -> str: + return self.username diff --git a/allianceauth/services/modules/phpbb3/tasks.py b/allianceauth/services/modules/phpbb3/tasks.py index 88d9d3bb..5d64812f 100644 --- a/allianceauth/services/modules/phpbb3/tasks.py +++ b/allianceauth/services/modules/phpbb3/tasks.py @@ -49,9 +49,9 @@ class Phpbb3Tasks: logger.debug(f"Updating user {user} phpbb3 groups to {groups}") try: Phpbb3Manager.update_groups(user.phpbb3.username, groups) - except Exception: + except Exception as e: logger.exception(f"Phpbb group sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e logger.debug(f"Updated user {user} phpbb3 groups.") else: logger.debug("User does not have a Phpbb3 account") diff --git a/allianceauth/services/modules/smf/tasks.py b/allianceauth/services/modules/smf/tasks.py index 9cac03a3..e57b1d1e 100644 --- a/allianceauth/services/modules/smf/tasks.py +++ b/allianceauth/services/modules/smf/tasks.py @@ -53,9 +53,9 @@ class SmfTasks: logger.debug(f"Updating user {user} smf groups to {groups}") try: SmfManager.update_groups(user.smf.username, groups) - except Exception: + except Exception as e: logger.exception(f"smf group sync failed for {user}, retrying in 10 mins") - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e logger.debug(f"Updated user {user} smf groups.") else: logger.debug("User does not have an smf account") @@ -77,11 +77,11 @@ class SmfTasks: f"SMF displayed name sync failed for {user}, " "user does not have a SMF account" ) - except Exception: + except Exception as e: logger.exception( f"SMF displayed name sync failed for {user}, retrying in 10 mins" ) - raise self.retry(countdown=60 * 10) + raise self.retry(exc=e, countdown=60 * 10) from e else: logger.debug(f"User {user} does not have a SMF account, skipping") diff --git a/allianceauth/services/modules/teamspeak3/tasks.py b/allianceauth/services/modules/teamspeak3/tasks.py index 61b0792f..046f9eed 100644 --- a/allianceauth/services/modules/teamspeak3/tasks.py +++ b/allianceauth/services/modules/teamspeak3/tasks.py @@ -81,7 +81,7 @@ class Teamspeak3Tasks: logger.debug(f"Updated user {user} teamspeak3 groups.") except TeamspeakError as e: logger.error(f"Error occured while syncing TS groups for {user}: {str(e)}") - raise self.retry(countdown=60*10) + raise self.retry(exc=e, countdown=60 * 10) from e else: logger.debug("User does not have a teamspeak3 account") diff --git a/allianceauth/services/modules/teamspeak3/util/ts3.py b/allianceauth/services/modules/teamspeak3/util/ts3.py index 358a1b58..bad23fbd 100644 --- a/allianceauth/services/modules/teamspeak3/util/ts3.py +++ b/allianceauth/services/modules/teamspeak3/util/ts3.py @@ -182,7 +182,7 @@ class TS3Proto: """ if isinstance(value, int): - return "%d" % value + return f"{value}" value = value.replace("\\", r'\\') for i, j in ts3_escape.items(): value = value.replace(i, j) @@ -197,7 +197,7 @@ class TS3Proto: """ if isinstance(value, int): - return "%d" % value + return f"{value}" value = value.replace(r"\\", "\\") for i, j in ts3_escape.items(): value = value.replace(j, i) diff --git a/allianceauth/services/signals.py b/allianceauth/services/signals.py index 167aadd8..2fb97740 100644 --- a/allianceauth/services/signals.py +++ b/allianceauth/services/signals.py @@ -68,8 +68,8 @@ def m2m_changed_group_permissions(sender, instance, action, pk_set, *args, **kwa logger.debug(f"Received m2m_changed from group {instance} permissions with action {action}") if instance.pk and (action == "post_remove" or action == "post_clear"): logger.debug(f"Checking if service permission changed for group {instance}") - # As validating an entire groups service could lead to many thousands of permission checks - # first we check that one of the permissions changed is, in fact, a service permission. + # As validating an entire group's service could lead to many thousands of permission checks, + # first, we check that one of the permissions changed is, in fact, a service permission. perms = Permission.objects.filter(pk__in=pk_set) got_change = False service_perms = [svc.access_perm for svc in ServicesHook.get_services()] @@ -81,18 +81,19 @@ def m2m_changed_group_permissions(sender, instance, action, pk_set, *args, **kwa continue for svc in ServicesHook.get_services(): if svc.access_perm == path_perm: - logger.debug(f"Permissions changed for group {instance} on service {svc}, re-validating services for groups users") + logger.debug(f"Permissions changed for group {instance} on service {svc}, re-validating services for group users") - def validate_all_groups_users_for_service(): - logger.debug(f"Performing validation for service {svc}") + def validate_all_groups_users_for_service(service): + logger.debug(f"Performing validation for service {service}") for user in instance.user_set.all(): - svc.validate_user(user) + service.validate_user(user) - transaction.on_commit(validate_all_groups_users_for_service) + transaction.on_commit(lambda service=svc: validate_all_groups_users_for_service(service)) got_change = True break # Found service, break out of services iteration and go back to permission iteration if not got_change: - logger.debug(f"Permission change for group {instance} was not service permission, ignoring") + logger.debug(f"Permission change for group {instance} was not a service permission, ignoring") + @receiver(m2m_changed, sender=State.permissions.through) @@ -115,12 +116,12 @@ def m2m_changed_state_permissions(sender, instance, action, pk_set, *args, **kwa if svc.access_perm == path_perm: logger.debug(f"Permissions changed for state {instance} on service {svc}, re-validating services for state users") - def validate_all_state_users_for_service(): - logger.debug(f"Performing validation for service {svc}") + def validate_all_state_users_for_service(service): + logger.debug(f"Performing validation for service {service}") for profile in instance.userprofile_set.all(): - svc.validate_user(profile.user) + service.validate_user(profile.user) - transaction.on_commit(validate_all_state_users_for_service) + transaction.on_commit(lambda service=svc: validate_all_state_users_for_service(service)) got_change = True break # Found service, break out of services iteration and go back to permission iteration if not got_change: From 8489f204ddb42e0886fc1da074ca9945e64edd5f Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Wed, 4 Dec 2024 22:10:06 +1000 Subject: [PATCH 036/178] fix test patch --- allianceauth/eveonline/tests/test_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/eveonline/tests/test_tasks.py b/allianceauth/eveonline/tests/test_tasks.py index 78054e8f..26dc9d90 100644 --- a/allianceauth/eveonline/tests/test_tasks.py +++ b/allianceauth/eveonline/tests/test_tasks.py @@ -84,7 +84,7 @@ class TestUpdateTasks(TestCase): @override_settings(CELERY_ALWAYS_EAGER=True) @patch('allianceauth.eveonline.providers.esi_client_factory') @patch('allianceauth.eveonline.tasks.providers') -@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2) +@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2) class TestRunModelUpdate(TransactionTestCase): def test_should_run_updates(self, mock_providers, mock_esi_client_factory): # given @@ -139,7 +139,7 @@ class TestRunModelUpdate(TransactionTestCase): @patch('allianceauth.eveonline.tasks.update_character', wraps=update_character) @patch('allianceauth.eveonline.providers.esi_client_factory') @patch('allianceauth.eveonline.tasks.providers') -@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2) +@patch('allianceauth.eveonline.tasks.CHARACTER_AFFILIATION_CHUNK_SIZE', 2) class TestUpdateCharacterChunk(TestCase): @staticmethod def _updated_character_ids(spy_update_character) -> set: From a3c6d5345b97a501fa16f11f840503cd632a271e Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 5 Dec 2024 11:07:44 +1000 Subject: [PATCH 037/178] These are warnings, id needs to be unique --- allianceauth/checks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index 4a994039..bb178b99 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -44,9 +44,9 @@ def system_package_redis(app_configs, **kwargs) -> list[CheckMessage]: errors.append(Warning("Unable to confirm Redis Version")) return errors if redis_version.major == 7 and redis_version.minor == 4 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=datetime.timezone.utc): - errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001")) + errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A001")) elif redis_version.major == 7 and redis_version.minor == 2: - errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A019")) + errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A021")) elif redis_version.major == 7 and redis_version.minor == 0: errors.append(Error(f"Redis {redis_version.public} EOL", hint="https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools", id="allianceauth.checks.A019")) elif redis_version.major == 6 and redis_version.minor == 2: From dc1ed8c570e161cd43b5218a7efffe09c94453c9 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 5 Dec 2024 11:48:45 +1000 Subject: [PATCH 038/178] +x --- docker/conf/redis_healthcheck.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/conf/redis_healthcheck.sh diff --git a/docker/conf/redis_healthcheck.sh b/docker/conf/redis_healthcheck.sh old mode 100644 new mode 100755 From a868438492e01580f396e2d1fcbc60d397667011 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 5 Dec 2024 11:49:32 +1000 Subject: [PATCH 039/178] force these flags on setup --- docker/scripts/download.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/scripts/download.sh b/docker/scripts/download.sh index 833b254c..f8a4526c 100755 --- a/docker/scripts/download.sh +++ b/docker/scripts/download.sh @@ -1,4 +1,6 @@ #!/bin/bash git clone https://gitlab.com/allianceauth/allianceauth.git aa-git cp -R aa-git/docker ./aa-docker +chmod +x aa-docker/conf/memory_check.sh +chmod +x aa-docker/conf/redis_healthcheck.sh rm -rf aa-git From 0a940810bde31a54d7d124ec6388980c7b5ba48f Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Thu, 5 Dec 2024 11:49:54 +1000 Subject: [PATCH 040/178] dont need this now the flag is set correctly, more consistent --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1c427652..c6917fa5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -83,7 +83,7 @@ services: - "redis-data:/data" - ./conf/redis_healthcheck.sh:/usr/local/bin/redis_healthcheck.sh healthcheck: - test: ["CMD", "bash", "/usr/local/bin/redis_healthcheck.sh"] + test: ["CMD", "/usr/local/bin/redis_healthcheck.sh"] logging: driver: "json-file" options: From 4da67cfaf62dc0ccf808ef51282a72360787ad64 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 8 Dec 2024 13:01:59 -0600 Subject: [PATCH 041/178] fix group display for Groups that are Group Leaders --- .../groupmanagement/templates/groupmanagement/groups.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/groupmanagement/templates/groupmanagement/groups.html b/allianceauth/groupmanagement/templates/groupmanagement/groups.html index ddd2c2d1..56aafafe 100644 --- a/allianceauth/groupmanagement/templates/groupmanagement/groups.html +++ b/allianceauth/groupmanagement/templates/groupmanagement/groups.html @@ -56,7 +56,7 @@ {% endif %} {% endfor %} {% endif %} - {% if g.group.authgroup.group_leaders.all.count %} + {% if g.group.authgroup.group_leader_groups.all.count %} {% for group in g.group.authgroup.group_leader_groups.all %} {{group.name}} {% endfor %} From 9ada26e849efceeec721d31c9cb5fba6ffc8c17a Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Mon, 9 Dec 2024 23:54:33 +0000 Subject: [PATCH 042/178] DockerMariaDB Config Template --- docker/conf/aa_mariadb.cnf | 6 ++++++ docker/docker-compose.yml | 1 + 2 files changed, 7 insertions(+) create mode 100644 docker/conf/aa_mariadb.cnf diff --git a/docker/conf/aa_mariadb.cnf b/docker/conf/aa_mariadb.cnf new file mode 100644 index 00000000..737da0c6 --- /dev/null +++ b/docker/conf/aa_mariadb.cnf @@ -0,0 +1,6 @@ +[mariadb] +# Provided as an Example +# AA Doesnt use Aria or MyISAM, So these are worth Considering + +# aria_pagecache_buffer_size = 16M +# key_buffer_size = 16M diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1c427652..d541113b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,6 +49,7 @@ services: volumes: - ./mysql-data:/var/lib/mysql - ./setup.sql:/docker-entrypoint-initdb.d/setup.sql + - ./conf/aa_mariadb.cnf:/etc/mysql/conf.d/aa_mariadb.cnf environment: - MYSQL_ROOT_PASSWORD=${AA_DB_ROOT_PASSWORD?err} - MARIADB_MYSQL_LOCALHOST_USER=1 From 7033406ba6b526eb16bcb1b22b1a35756bdf1246 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Tue, 10 Dec 2024 13:07:03 +1000 Subject: [PATCH 043/178] Version Bump 4.5.0 --- allianceauth/__init__.py | 2 +- docker/.env.example | 2 +- docker/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/allianceauth/__init__.py b/allianceauth/__init__.py index 77d54c77..261f884e 100644 --- a/allianceauth/__init__.py +++ b/allianceauth/__init__.py @@ -5,7 +5,7 @@ manage online service access. # This will make sure the app is always imported when # Django starts so that shared_task will use this app. -__version__ = '4.4.2' +__version__ = '4.5.0' __title__ = 'Alliance Auth' __url__ = 'https://gitlab.com/allianceauth/allianceauth' NAME = f'{__title__} v{__version__}' diff --git a/docker/.env.example b/docker/.env.example index 74712116..61e77a19 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,7 +1,7 @@ PROTOCOL=https:// AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN% DOMAIN=%DOMAIN% -AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.4.2 +AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.5.0 # Nginx Proxy Manager PROXY_HTTP_PORT=80 diff --git a/docker/Dockerfile b/docker/Dockerfile index 462f6a56..6e88124c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.11-slim -ARG AUTH_VERSION=v4.4.2 +ARG AUTH_VERSION=v4.5.0 ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION} ENV AUTH_USER=allianceauth ENV AUTH_GROUP=allianceauth From d61a49f2d97ff4c2cf7a9f18e76c272bd1b838d7 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Thu, 19 Dec 2024 02:36:46 +0000 Subject: [PATCH 044/178] Use lts tag for mariadb docker --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c9eca829..d221335c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -38,7 +38,7 @@ x-allianceauth-health-check: &allianceauth-health-checks services: auth_mysql: - image: mariadb:11.4 + image: mariadb:lts command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --default-authentication-plugin=mysql_native_password] volumes: - ./mysql-data:/var/lib/mysql From 9df76443b1cd0f8d83184154e60a333111a17c23 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 28 Dec 2024 13:35:24 +1000 Subject: [PATCH 045/178] correct discourse user updater --- allianceauth/services/modules/discourse/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/services/modules/discourse/manager.py b/allianceauth/services/modules/discourse/manager.py index 95f9084d..d50f39a6 100644 --- a/allianceauth/services/modules/discourse/manager.py +++ b/allianceauth/services/modules/discourse/manager.py @@ -108,9 +108,9 @@ class DiscourseManager: providers.discourse.client.activate(u_id) @staticmethod - def __update_user(username, **kwargs): + def __update_user(username, **kwargs) -> None: u_id = DiscourseManager.__user_name_to_id(username) - providers.discourse.client.update_user(endpoint, u_id, **kwargs) + providers.discourse.client.update_user(username, u_id, **kwargs) @staticmethod def __create_user(username, email, password): From 168b023a72aa90d83abd6d3064dae45e1907a4ef Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 28 Dec 2024 13:41:39 +1000 Subject: [PATCH 046/178] u_id param not needed --- allianceauth/services/modules/discourse/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/allianceauth/services/modules/discourse/manager.py b/allianceauth/services/modules/discourse/manager.py index d50f39a6..f6756788 100644 --- a/allianceauth/services/modules/discourse/manager.py +++ b/allianceauth/services/modules/discourse/manager.py @@ -109,8 +109,7 @@ class DiscourseManager: @staticmethod def __update_user(username, **kwargs) -> None: - u_id = DiscourseManager.__user_name_to_id(username) - providers.discourse.client.update_user(username, u_id, **kwargs) + providers.discourse.client.update_user(username, **kwargs) @staticmethod def __create_user(username, email, password): From d10562e9fcd2354463dcf4be1782276588fbb962 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Sat, 28 Dec 2024 14:15:09 +1000 Subject: [PATCH 047/178] remove more bs3 fallback remnants --- allianceauth/authentication/urls.py | 1 - allianceauth/authentication/views.py | 9 ------ .../menu/tests/integration/test_dashboard.py | 32 ------------------- 3 files changed, 42 deletions(-) diff --git a/allianceauth/authentication/urls.py b/allianceauth/authentication/urls.py index 72bc1e10..9f9075a7 100644 --- a/allianceauth/authentication/urls.py +++ b/allianceauth/authentication/urls.py @@ -38,7 +38,6 @@ urlpatterns = [ name='token_refresh' ), path('dashboard/', views.dashboard, name='dashboard'), - path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'), path('task-counts/', views.task_counts, name='task_counts'), path('esi-check/', views.esi_check, name='esi_check'), ] diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 392a0a44..ba311cfa 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -392,12 +392,3 @@ def esi_check(request) -> JsonResponse: "data": check_for_override_esi_error_message(_r) } return JsonResponse(data) - - -@login_required -def dashboard_bs3(request): - """Render dashboard view with BS3 theme. - - This is an internal view used for testing BS3 backward compatibility in AA4 only. - """ - return render(request, 'authentication/dashboard_bs3.html') diff --git a/allianceauth/menu/tests/integration/test_dashboard.py b/allianceauth/menu/tests/integration/test_dashboard.py index 876f3173..9d083eaf 100644 --- a/allianceauth/menu/tests/integration/test_dashboard.py +++ b/allianceauth/menu/tests/integration/test_dashboard.py @@ -68,35 +68,3 @@ class TestDefaultDashboardWithSideMenu(TestCase): self.assertEqual(links["/dashboard/"], "Dashboard") self.assertEqual(links["/groups/"], "Groups") self.assertNotIn("http://www.example.com/alpha", links) - - -class TestBS3DashboardWithSideMenu(TestCase): - def test_should_not_show_group_management_when_user_has_no_permission(self): - # given - user = create_user() - self.client.force_login(user) - - # when - response = self.client.get("/dashboard_bs3/") - - # then - self.assertEqual(response.status_code, HTTPStatus.OK) - links = extract_links(response) - self.assertEqual(links["/dashboard/"], "Dashboard") - self.assertEqual(links["/groups/"], "Groups") - self.assertNotIn("/groupmanagement/requests/", links) - - def test_should_show_group_management_when_user_has_permission(self): - # given - user = create_user(permissions=["auth.group_management"]) - self.client.force_login(user) - - # when - response = self.client.get("/dashboard_bs3/") - - # then - self.assertEqual(response.status_code, HTTPStatus.OK) - links = extract_links(response) - self.assertEqual(links["/dashboard/"], "Dashboard") - self.assertEqual(links["/groups/"], "Groups") - self.assertEqual(links["/groupmanagement/requests/"], "Group Management") From 0f057ffa84f2f627690d99326b6c824fda03c11d Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Thu, 2 Jan 2025 16:54:37 +0100 Subject: [PATCH 048/178] [ADD] `django-sri` to provide integrity hashes for local static files --- .../project_template/project_name/settings/base.py | 2 ++ allianceauth/templates/bundles/auth-base-css.html | 4 ++-- allianceauth/templates/bundles/auth-framework-css.html | 4 ++-- allianceauth/templates/bundles/bootstrap-css.html | 6 ++++-- allianceauth/templates/bundles/checkbox-css.html | 4 ++-- allianceauth/templates/bundles/evetime-js.html | 4 ++-- allianceauth/templates/bundles/filterdropdown-js.html | 4 ++-- allianceauth/templates/bundles/jquery-ui-css.html | 7 ++++--- .../templates/bundles/refresh-notification-icon-js.html | 4 ++-- .../templates/bundles/refresh-notifications-js.html | 4 ++-- allianceauth/templates/bundles/timerboard-js.html | 4 ++-- allianceauth/templates/bundles/timers-js.html | 4 ++-- pyproject.toml | 1 + 13 files changed, 29 insertions(+), 23 deletions(-) diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py index fb18e4de..9e8d6957 100644 --- a/allianceauth/project_template/project_name/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -43,8 +43,10 @@ INSTALLED_APPS = [ 'allianceauth.theme.flatly', 'allianceauth.theme.materia', "allianceauth.custom_css", + 'sri', ] +SRI_ALGORITHM = "sha512" SECRET_KEY = "wow I'm a really bad default secret key" # Celery configuration diff --git a/allianceauth/templates/bundles/auth-base-css.html b/allianceauth/templates/bundles/auth-base-css.html index 0ee2e7ce..ce7b4373 100644 --- a/allianceauth/templates/bundles/auth-base-css.html +++ b/allianceauth/templates/bundles/auth-base-css.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/css/auth-base.css' %} diff --git a/allianceauth/templates/bundles/auth-framework-css.html b/allianceauth/templates/bundles/auth-framework-css.html index 9f1c038a..1722377a 100644 --- a/allianceauth/templates/bundles/auth-framework-css.html +++ b/allianceauth/templates/bundles/auth-framework-css.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/framework/css/auth-framework.css' %} diff --git a/allianceauth/templates/bundles/bootstrap-css.html b/allianceauth/templates/bundles/bootstrap-css.html index bed760ba..88147119 100644 --- a/allianceauth/templates/bundles/bootstrap-css.html +++ b/allianceauth/templates/bundles/bootstrap-css.html @@ -1,4 +1,6 @@ {% load static %} +{% load sri %} + {% if NIGHT_MODE %} {% if debug %} @@ -6,7 +8,7 @@ {% else %} - + {% sri_static 'allianceauth/css/themes/darkly/darkly.min.css' %} {% endif %} {% else %} {% if debug %} @@ -14,7 +16,7 @@ {% else %} - + {% sri_static 'allianceauth/css/themes/flatly/flatly.min.css' %} {% endif %} {% endif %} diff --git a/allianceauth/templates/bundles/checkbox-css.html b/allianceauth/templates/bundles/checkbox-css.html index f400d36c..1f2cc08e 100644 --- a/allianceauth/templates/bundles/checkbox-css.html +++ b/allianceauth/templates/bundles/checkbox-css.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/css/checkbox.css' %} diff --git a/allianceauth/templates/bundles/evetime-js.html b/allianceauth/templates/bundles/evetime-js.html index 63b712c3..6611516e 100644 --- a/allianceauth/templates/bundles/evetime-js.html +++ b/allianceauth/templates/bundles/evetime-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/eve-time.js' %} diff --git a/allianceauth/templates/bundles/filterdropdown-js.html b/allianceauth/templates/bundles/filterdropdown-js.html index 677fa536..41a0e3bd 100644 --- a/allianceauth/templates/bundles/filterdropdown-js.html +++ b/allianceauth/templates/bundles/filterdropdown-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/filterDropDown/filterDropDown.min.js' %} diff --git a/allianceauth/templates/bundles/jquery-ui-css.html b/allianceauth/templates/bundles/jquery-ui-css.html index 3bf4d1a3..9177c19e 100644 --- a/allianceauth/templates/bundles/jquery-ui-css.html +++ b/allianceauth/templates/bundles/jquery-ui-css.html @@ -1,5 +1,6 @@ -{% load static %} - - + +{% load sri %} + +{% sri_static 'allianceauth/js/jquery-ui/1.13.2/css/jquery-ui.min.css' %} diff --git a/allianceauth/templates/bundles/refresh-notification-icon-js.html b/allianceauth/templates/bundles/refresh-notification-icon-js.html index 88f3d16f..69cc53b8 100644 --- a/allianceauth/templates/bundles/refresh-notification-icon-js.html +++ b/allianceauth/templates/bundles/refresh-notification-icon-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/refresh-notification-icon.js' %} diff --git a/allianceauth/templates/bundles/refresh-notifications-js.html b/allianceauth/templates/bundles/refresh-notifications-js.html index 27622b3e..50b5e8f4 100644 --- a/allianceauth/templates/bundles/refresh-notifications-js.html +++ b/allianceauth/templates/bundles/refresh-notifications-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/refresh_notifications.js' %} diff --git a/allianceauth/templates/bundles/timerboard-js.html b/allianceauth/templates/bundles/timerboard-js.html index 8cf6f353..8bad97d9 100644 --- a/allianceauth/templates/bundles/timerboard-js.html +++ b/allianceauth/templates/bundles/timerboard-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/timerboard.js' %} diff --git a/allianceauth/templates/bundles/timers-js.html b/allianceauth/templates/bundles/timers-js.html index b9690202..a570a10d 100644 --- a/allianceauth/templates/bundles/timers-js.html +++ b/allianceauth/templates/bundles/timers-js.html @@ -1,3 +1,3 @@ -{% load static %} +{% load sri %} - +{% sri_static 'allianceauth/js/timers.js' %} diff --git a/pyproject.toml b/pyproject.toml index 35edbe64..3ed04b9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ dependencies = [ "django-registration<3.4,>=3.3", "django-solo", "django-sortedm2m", + "django-sri", "dnspython", "mysqlclient>=2.1", "openfire-restapi", From 646d3f5408b261ff3e50b9baae85f828d899b738 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 16:21:14 +0100 Subject: [PATCH 049/178] [ADD] Mercenary Dens to structure type selection --- allianceauth/timerboard/models.py | 1 + allianceauth/timerboard/templates/timerboard/timertable.html | 2 ++ 2 files changed, 3 insertions(+) diff --git a/allianceauth/timerboard/models.py b/allianceauth/timerboard/models.py index d4127c9b..e0713e88 100644 --- a/allianceauth/timerboard/models.py +++ b/allianceauth/timerboard/models.py @@ -39,6 +39,7 @@ class Timer(models.Model): PHAROLUX = "Pharolux Cyno Beacon", _("Pharolux Cyno Beacon") TENEBREX = "Tenebrex Cyno Jammer", _("Tenebrex Cyno Jammer") ANSIBLEX = "Ansiblex Jump Gate", _("Ansiblex Jump Gate") + MERCDEN = "Mercenary Den", _("Mercenary Den") MOONPOP = "Moon Mining Cycle", _("Moon Mining Cycle") METENOX = "Metenox Moon Drill", _("Metenox Moon Drill") OTHER = "Other", _("Other") diff --git a/allianceauth/timerboard/templates/timerboard/timertable.html b/allianceauth/timerboard/templates/timerboard/timertable.html index c1d5c818..7ac0099c 100644 --- a/allianceauth/timerboard/templates/timerboard/timertable.html +++ b/allianceauth/timerboard/templates/timerboard/timertable.html @@ -80,6 +80,8 @@
    {% translate "Cyno Jammer" %}
    {% elif timer.structure == "Jump Gate" or timer.structure == "Ansiblex Jump Gate" %}
    {% translate "Ansiblex Jump Gate" %}
    + {% elif timer.structure == "Mercenary Den" %} +
    {% translate "Mercenary Den" %}
    {% elif timer.structure == "Moon Mining Cycle" %}
    {% translate "Moon Mining Cycle" %}
    {% elif timer.structure == "Metenox Moon Drill" %} From a4ea48e14e25477eb41728b44d30a542b91ad13e Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 16:43:59 +0100 Subject: [PATCH 050/178] [CHANGE] Group timer types by BG color BS class --- .../templates/timerboard/timertable.html | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/allianceauth/timerboard/templates/timerboard/timertable.html b/allianceauth/timerboard/templates/timerboard/timertable.html index 7ac0099c..3ee2c202 100644 --- a/allianceauth/timerboard/templates/timerboard/timertable.html +++ b/allianceauth/timerboard/templates/timerboard/timertable.html @@ -30,13 +30,21 @@ + {% comment %} Objective: Hostile (BG: Danger) {% endcomment %} {% if timer.objective == "Hostile" %} -
    {% translate "Hostile" %}
    +
    + + {% comment %} Objective: Friendly (BG: Primare) {% endcomment %} {% elif timer.objective == "Friendly" %} -
    {% translate "Friendly" %}
    +
    + + {% comment %} Objective: Neutral (BG: Secondary) {% endcomment %} {% elif timer.objective == "Neutral" %} -
    {% translate "Neutral" %}
    +
    {% endif %} + + {{ timer.get_objective_display }} +
    @@ -44,32 +52,27 @@ + {% comment %} BG: Info {% endcomment %} {% if timer.structure == "POCO" %}
    {% translate "POCO" %}
    - {% elif timer.structure == "Orbital Skyhook" %} -
    {% translate "Orbital Skyhook" %}
    - {% elif timer.structure == "I-HUB" %} -
    {% translate "I-HUB" %}
    - {% elif timer.structure == "TCU" %} {% comment %} Pending Removal {% endcomment %} -
    {% translate "TCU" %}
    {% elif timer.structure == "POS[S]" %}
    {% translate "POS [S]" %}
    {% elif timer.structure == "POS[M]" %}
    {% translate "POS [M]" %}
    {% elif timer.structure == "POS[L]" %}
    {% translate "POS [L]" %}
    - {% elif timer.structure == "Citadel[M]" or timer.structure == "Astrahus" %} -
    {% translate "Astrahus" %}
    - {% elif timer.structure == "Citadel[L]" or timer.structure == "Fortizar" %} -
    {% translate "Fortizar" %}
    - {% elif timer.structure == "Citadel[XL]" or timer.structure == "Keepstar" %} -
    {% translate "Keepstar" %}
    + + {% comment %} BG: Warning {% endcomment %} + {% elif timer.structure == "Station" %} +
    {% translate "Station" %}
    + {% elif timer.structure == "Orbital Skyhook" %} +
    {% translate "Orbital Skyhook" %}
    + {% elif timer.structure == "I-HUB" %} +
    {% translate "I-HUB" %}
    {% elif timer.structure == "Engineering Complex[M]" or timer.structure == "Raitaru" %}
    {% translate "Raitaru" %}
    {% elif timer.structure == "Engineering Complex[L]" or timer.structure == "Azbel" %}
    {% translate "Azbel" %}
    - {% elif timer.structure == "Engineering Complex[XL]" or timer.structure == "Sotiyo" %} -
    {% translate "Sotiyo" %}
    {% elif timer.structure == "Refinery[M]" or timer.structure == "Athanor" %}
    {% translate "Athanor" %}
    {% elif timer.structure == "Refinery[L]" or timer.structure == "Tatara" %} @@ -82,12 +85,30 @@
    {% translate "Ansiblex Jump Gate" %}
    {% elif timer.structure == "Mercenary Den" %}
    {% translate "Mercenary Den" %}
    - {% elif timer.structure == "Moon Mining Cycle" %} -
    {% translate "Moon Mining Cycle" %}
    {% elif timer.structure == "Metenox Moon Drill" %}
    {% translate "Metenox Moon Drill" %}
    + + {% comment %} BG: Danger {% endcomment %} + {% elif timer.structure == "TCU" %} +
    {% translate "TCU" %}
    + {% elif timer.structure == "Citadel[M]" or timer.structure == "Astrahus" %} +
    {% translate "Astrahus" %}
    + {% elif timer.structure == "Citadel[L]" or timer.structure == "Fortizar" %} +
    {% translate "Fortizar" %}
    + {% elif timer.structure == "Citadel[XL]" or timer.structure == "Keepstar" %} +
    {% translate "Keepstar" %}
    + {% elif timer.structure == "Engineering Complex[XL]" or timer.structure == "Sotiyo" %} +
    {% translate "Sotiyo" %}
    + + {% comment %} BG: Secondary {% endcomment %} + {% elif timer.structure == "TCU" %} {% comment %} Pending Removal {% endcomment %} +
    {% translate "TCU" %}
    {% elif timer.structure == "Other" %}
    {% translate "Other" %}
    + + {% comment %} BG: Success {% endcomment %} + {% elif timer.structure == "Moon Mining Cycle" %} +
    {% translate "Moon Mining Cycle" %}
    {% endif %} From bd4dd60c989d087547b18d155a66f0b04ed1795f Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 16:48:02 +0100 Subject: [PATCH 051/178] [CHANGE] Rename I-Hub to Sovereignty Hub --- allianceauth/timerboard/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/timerboard/models.py b/allianceauth/timerboard/models.py index e0713e88..ce14f17e 100644 --- a/allianceauth/timerboard/models.py +++ b/allianceauth/timerboard/models.py @@ -23,7 +23,7 @@ class Timer(models.Model): POCO = "POCO", _("POCO") ORBITALSKYHOOK = "Orbital Skyhook", _("Orbital Skyhook") - IHUB = "I-HUB", _("I-HUB") + IHUB = "I-HUB", _("Soverreignty Hub") TCU = "TCU", _("TCU") # Pending Remval POSS = "POS[S]", _("POS [S]") POSM = "POS[M]", _("POS [M]") From f51523dc07d768ccbdd74a296459172bf7e268d7 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 17:03:00 +0100 Subject: [PATCH 052/178] [CHANGE] Use TextChoices displayed name in template --- allianceauth/timerboard/models.py | 6 +- .../templates/timerboard/timertable.html | 80 ++++++++++--------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/allianceauth/timerboard/models.py b/allianceauth/timerboard/models.py index ce14f17e..6823ea8d 100644 --- a/allianceauth/timerboard/models.py +++ b/allianceauth/timerboard/models.py @@ -1,6 +1,6 @@ from django.contrib.auth.models import User from django.db import models -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCorporationInfo @@ -36,8 +36,8 @@ class Timer(models.Model): SOTIYO = "Sotiyo", _("Sotiyo") ATHANOR = "Athanor", _("Athanor") TATARA = "Tatara", _("Tatara") - PHAROLUX = "Pharolux Cyno Beacon", _("Pharolux Cyno Beacon") - TENEBREX = "Tenebrex Cyno Jammer", _("Tenebrex Cyno Jammer") + PHAROLUX = "Pharolux Cyno Beacon", _("Cyno Beacon") + TENEBREX = "Tenebrex Cyno Jammer", _("Cyno Jammer") ANSIBLEX = "Ansiblex Jump Gate", _("Ansiblex Jump Gate") MERCDEN = "Mercenary Den", _("Mercenary Den") MOONPOP = "Moon Mining Cycle", _("Moon Mining Cycle") diff --git a/allianceauth/timerboard/templates/timerboard/timertable.html b/allianceauth/timerboard/templates/timerboard/timertable.html index 3ee2c202..455d0d94 100644 --- a/allianceauth/timerboard/templates/timerboard/timertable.html +++ b/allianceauth/timerboard/templates/timerboard/timertable.html @@ -19,7 +19,6 @@ {% for timer in timers %} - {{ timer.details }} @@ -54,62 +53,69 @@ {% comment %} BG: Info {% endcomment %} {% if timer.structure == "POCO" %} -
    {% translate "POCO" %}
    +
    {% elif timer.structure == "POS[S]" %} -
    {% translate "POS [S]" %}
    +
    {% elif timer.structure == "POS[M]" %} -
    {% translate "POS [M]" %}
    +
    {% elif timer.structure == "POS[L]" %} -
    {% translate "POS [L]" %}
    +
    {% comment %} BG: Warning {% endcomment %} - {% elif timer.structure == "Station" %} -
    {% translate "Station" %}
    + {% elif timer.structure == "Station" %} {% comment %} Pending Removal {% endcomment %} +
    {% elif timer.structure == "Orbital Skyhook" %} -
    {% translate "Orbital Skyhook" %}
    +
    {% elif timer.structure == "I-HUB" %} -
    {% translate "I-HUB" %}
    - {% elif timer.structure == "Engineering Complex[M]" or timer.structure == "Raitaru" %} -
    {% translate "Raitaru" %}
    - {% elif timer.structure == "Engineering Complex[L]" or timer.structure == "Azbel" %} -
    {% translate "Azbel" %}
    - {% elif timer.structure == "Refinery[M]" or timer.structure == "Athanor" %} -
    {% translate "Athanor" %}
    - {% elif timer.structure == "Refinery[L]" or timer.structure == "Tatara" %} -
    {% translate "Tatara" %}
    - {% elif timer.structure == "Cyno Beacon" or timer.structure == "Pharolux Cyno Beacon" %} -
    {% translate "Cyno Beacon" %}
    - {% elif timer.structure == "Cyno Jammer" or timer.structure == "Tenebrex Cyno Jammer" %} -
    {% translate "Cyno Jammer" %}
    - {% elif timer.structure == "Jump Gate" or timer.structure == "Ansiblex Jump Gate" %} -
    {% translate "Ansiblex Jump Gate" %}
    +
    + {% elif timer.structure == "Raitaru" %} +
    + {% elif timer.structure == "Azbel" %} +
    + {% elif timer.structure == "Athanor" %} +
    + {% elif timer.structure == "Tatara" %} +
    + {% elif timer.structure == "Pharolux Cyno Beacon" %} +
    + {% elif timer.structure == "Tenebrex Cyno Jammer" %} +
    + {% elif timer.structure == "Ansiblex Jump Gate" %} +
    {% elif timer.structure == "Mercenary Den" %} -
    {% translate "Mercenary Den" %}
    +
    {% elif timer.structure == "Metenox Moon Drill" %} -
    {% translate "Metenox Moon Drill" %}
    +
    {% comment %} BG: Danger {% endcomment %} {% elif timer.structure == "TCU" %} -
    {% translate "TCU" %}
    - {% elif timer.structure == "Citadel[M]" or timer.structure == "Astrahus" %} -
    {% translate "Astrahus" %}
    - {% elif timer.structure == "Citadel[L]" or timer.structure == "Fortizar" %} -
    {% translate "Fortizar" %}
    - {% elif timer.structure == "Citadel[XL]" or timer.structure == "Keepstar" %} -
    {% translate "Keepstar" %}
    - {% elif timer.structure == "Engineering Complex[XL]" or timer.structure == "Sotiyo" %} -
    {% translate "Sotiyo" %}
    +
    + {% elif timer.structure == "Astrahus" %} +
    + {% elif timer.structure == "Fortizar" %} +
    + {% elif timer.structure == "Keepstar" %} +
    + {% elif timer.structure == "Sotiyo" %} +
    {% comment %} BG: Secondary {% endcomment %} {% elif timer.structure == "TCU" %} {% comment %} Pending Removal {% endcomment %} -
    {% translate "TCU" %}
    +
    {% elif timer.structure == "Other" %} -
    {% translate "Other" %}
    +
    {% comment %} BG: Success {% endcomment %} {% elif timer.structure == "Moon Mining Cycle" %} -
    {% translate "Moon Mining Cycle" %}
    +
    + + {% comment %} BG: Primary (for all other timers) {% endcomment %} + {% else %} +
    {% endif %} + + {{ timer.get_structure_display }} +
    {{ timer.eve_time | date:"Y-m-d H:i" }} From c6b6443901a086b6ee02b9f7a37ef79b3c2d0381 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 18:08:20 +0100 Subject: [PATCH 053/178] [CHANGE] Move structure label BG detection to Python instead of Django MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python is faster and needs less memory … It's also much mire readable that way. --- .../templates/timerboard/timertable.html | 64 +-------- allianceauth/timerboard/views.py | 135 +++++++++++++++--- 2 files changed, 114 insertions(+), 85 deletions(-) diff --git a/allianceauth/timerboard/templates/timerboard/timertable.html b/allianceauth/timerboard/templates/timerboard/timertable.html index 455d0d94..014b01e8 100644 --- a/allianceauth/timerboard/templates/timerboard/timertable.html +++ b/allianceauth/timerboard/templates/timerboard/timertable.html @@ -51,69 +51,7 @@ - {% comment %} BG: Info {% endcomment %} - {% if timer.structure == "POCO" %} -
    - {% elif timer.structure == "POS[S]" %} -
    - {% elif timer.structure == "POS[M]" %} -
    - {% elif timer.structure == "POS[L]" %} -
    - - {% comment %} BG: Warning {% endcomment %} - {% elif timer.structure == "Station" %} {% comment %} Pending Removal {% endcomment %} -
    - {% elif timer.structure == "Orbital Skyhook" %} -
    - {% elif timer.structure == "I-HUB" %} -
    - {% elif timer.structure == "Raitaru" %} -
    - {% elif timer.structure == "Azbel" %} -
    - {% elif timer.structure == "Athanor" %} -
    - {% elif timer.structure == "Tatara" %} -
    - {% elif timer.structure == "Pharolux Cyno Beacon" %} -
    - {% elif timer.structure == "Tenebrex Cyno Jammer" %} -
    - {% elif timer.structure == "Ansiblex Jump Gate" %} -
    - {% elif timer.structure == "Mercenary Den" %} -
    - {% elif timer.structure == "Metenox Moon Drill" %} -
    - - {% comment %} BG: Danger {% endcomment %} - {% elif timer.structure == "TCU" %} -
    - {% elif timer.structure == "Astrahus" %} -
    - {% elif timer.structure == "Fortizar" %} -
    - {% elif timer.structure == "Keepstar" %} -
    - {% elif timer.structure == "Sotiyo" %} -
    - - {% comment %} BG: Secondary {% endcomment %} - {% elif timer.structure == "TCU" %} {% comment %} Pending Removal {% endcomment %} -
    - {% elif timer.structure == "Other" %} -
    - - {% comment %} BG: Success {% endcomment %} - {% elif timer.structure == "Moon Mining Cycle" %} -
    - - {% comment %} BG: Primary (for all other timers) {% endcomment %} - {% else %} -
    - {% endif %} - +
    {{ timer.get_structure_display }}
    diff --git a/allianceauth/timerboard/views.py b/allianceauth/timerboard/views.py index 93c877c8..bfee13ff 100644 --- a/allianceauth/timerboard/views.py +++ b/allianceauth/timerboard/views.py @@ -3,7 +3,8 @@ import logging from django.contrib import messages from django.contrib.auth.mixins import ( - LoginRequiredMixin, PermissionRequiredMixin, + LoginRequiredMixin, + PermissionRequiredMixin, ) from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render @@ -20,8 +21,8 @@ from allianceauth.timerboard.models import Timer logger = logging.getLogger(__name__) -TIMER_VIEW_PERMISSION = 'auth.timer_view' -TIMER_MANAGE_PERMISSION = 'auth.timer_management' +TIMER_VIEW_PERMISSION = "auth.timer_view" +TIMER_MANAGE_PERMISSION = "auth.timer_management" class BaseTimerView(LoginRequiredMixin, PermissionRequiredMixin, View): @@ -29,22 +30,112 @@ class BaseTimerView(LoginRequiredMixin, PermissionRequiredMixin, View): class TimerView(BaseTimerView): - template_name = 'timerboard/view.html' + template_name = "timerboard/view.html" permission_required = TIMER_VIEW_PERMISSION def get(self, request): + """ + Renders the timer view + + :param request: + :type request: + :return: + :rtype: + """ + + def get_bg_modifier(structure): + """ + Returns the bootstrap bg modifier for the given structure + + :param structure: + :type structure: + :return: + :rtype: + """ + + if structure in bg_info: + return "info" + elif structure in bg_waning: + return "warning" + elif structure in bg_danger: + return "danger" + elif structure in bg_secondary: + return "secondary" + + return "primary" + logger.debug(f"timer_view called by user {request.user}") char = request.user.profile.main_character + if char: corp = char.corporation else: corp = None - base_query = Timer.objects.select_related('eve_character') + + base_query = Timer.objects.select_related("eve_character") + + timers = [] + corp_timers = [] + future_timers = [] + past_timers = [] + + bg_info = [ + Timer.Structure.POCO.value, # POCO + Timer.Structure.POSS.value, # POS[S] + Timer.Structure.POSM.value, # POS[M] + Timer.Structure.POSL.value, # POS[L] + ] + bg_waning = [ + Timer.Structure.ANSIBLEX.value, # Ansiblex Jump Gate + Timer.Structure.ATHANOR.value, # Athanor + Timer.Structure.AZBEL.value, # Azbel + Timer.Structure.MERCDEN.value, # Mercenary Den + Timer.Structure.METENOX.value, # Metenox Moon Drill + Timer.Structure.ORBITALSKYHOOK.value, # Orbital Skyhook + Timer.Structure.PHAROLUX.value, # Pharolux Cyno Beacon + Timer.Structure.RAITARU.value, # Raitaru + "Station", # Legacy structure, remove in future update + Timer.Structure.TATARA.value, # Tatara + Timer.Structure.TENEBREX.value, # Tenebrex Cyno Jammer + ] + bg_danger = [ + Timer.Structure.ASTRAHUS.value, # Astrahus + Timer.Structure.FORTIZAR.value, # Fortizar + Timer.Structure.IHUB.value, # I-HUB + Timer.Structure.KEEPSTAR.value, # Keepstar + Timer.Structure.SOTIYO.value, # Sotiyo + Timer.Structure.TCU.value, # TCU (Legacy structure, remove in future update) + ] + bg_secondary = [ + Timer.Structure.MOONPOP.value, # Moon Mining Cycle + Timer.Structure.OTHER.value, # Other + ] + + # Timers + for timer in base_query.filter(corp_timer=False): + timer.bg_modifier = get_bg_modifier(timer.structure) + timers.append(timer) + + # Corp Timers + for timer in base_query.filter(corp_timer=True, eve_corp=corp): + timer.bg_modifier = get_bg_modifier(timer.structure) + corp_timers.append(timer) + + # Future Timers + for timer in base_query.filter(corp_timer=False, eve_time__gte=timezone.now()): + timer.bg_modifier = get_bg_modifier(timer.structure) + future_timers.append(timer) + + # Past Timers + for timer in base_query.filter(corp_timer=False, eve_time__lt=timezone.now()): + timer.bg_modifier = get_bg_modifier(timer.structure) + past_timers.append(timer) + render_items = { - 'timers': base_query.filter(corp_timer=False), - 'corp_timers': base_query.filter(corp_timer=True, eve_corp=corp), - 'future_timers': base_query.filter(corp_timer=False, eve_time__gte=timezone.now()), - 'past_timers': base_query.filter(corp_timer=False, eve_time__lt=timezone.now()), + "timers": timers, + "corp_timers": corp_timers, + "future_timers": future_timers, + "past_timers": past_timers, } return render(request, self.template_name, context=render_items) @@ -52,7 +143,7 @@ class TimerView(BaseTimerView): class TimerManagementView(BaseTimerView): permission_required = TIMER_MANAGE_PERMISSION - index_redirect = 'timerboard:view' + index_redirect = "timerboard:view" success_url = reverse_lazy(index_redirect) model = Timer @@ -66,12 +157,12 @@ class AddUpdateMixin: Inject the request user into the kwargs passed to the form """ kwargs = super().get_form_kwargs() - kwargs.update({'user': self.request.user}) + kwargs.update({"user": self.request.user}) return kwargs class AddTimerView(TimerManagementView, AddUpdateMixin, CreateView): - template_name_suffix = '_create_form' + template_name_suffix = "_create_form" form_class = TimerForm def form_valid(self, form): @@ -82,17 +173,18 @@ class AddTimerView(TimerManagementView, AddUpdateMixin, CreateView): ) messages.success( self.request, - _('Added new timer in %(system)s at %(time)s.') % {"system": timer.system, "time": timer.eve_time} + _("Added new timer in %(system)s at %(time)s.") + % {"system": timer.system, "time": timer.eve_time}, ) return result class EditTimerView(TimerManagementView, AddUpdateMixin, UpdateView): - template_name_suffix = '_update_form' + template_name_suffix = "_update_form" form_class = TimerForm def form_valid(self, form): - messages.success(self.request, _('Saved changes to the timer.')) + messages.success(self.request, _("Saved changes to the timer.")) return super().form_valid(form) @@ -107,21 +199,20 @@ def dashboard_timers(request): except (EveCorporationInfo.DoesNotExist, AttributeError): return "" - timers = Timer.objects.select_related( - 'eve_character' - ).filter( + timers = Timer.objects.select_related("eve_character").filter( (Q(corp_timer=True) & Q(eve_corp=corp)) | Q(corp_timer=False), - eve_time__gte=timezone.now() + eve_time__gte=timezone.now(), )[:5] if timers.count(): context = { - 'timers': timers, + "timers": timers, } return render_to_string( - template_name='timerboard/dashboard.timers.html', - context=context, request=request + template_name="timerboard/dashboard.timers.html", + context=context, + request=request, ) else: return "" From b737504d52fea34e7274b0f5b7e4233110166c91 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 18:08:52 +0100 Subject: [PATCH 054/178] [REMOVE] Unused imports --- allianceauth/timerboard/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/allianceauth/timerboard/views.py b/allianceauth/timerboard/views.py index bfee13ff..27c6045f 100644 --- a/allianceauth/timerboard/views.py +++ b/allianceauth/timerboard/views.py @@ -1,4 +1,3 @@ -import datetime import logging from django.contrib import messages @@ -7,7 +6,7 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin, ) from django.db.models import Q -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils import timezone From 4578ecf21dbe79df29855a08256af88fe7ee2e8a Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 6 Jan 2025 18:17:21 +0100 Subject: [PATCH 055/178] [ADD] Missing migration --- .../migrations/0007_alter_timer_structure.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 allianceauth/timerboard/migrations/0007_alter_timer_structure.py diff --git a/allianceauth/timerboard/migrations/0007_alter_timer_structure.py b/allianceauth/timerboard/migrations/0007_alter_timer_structure.py new file mode 100644 index 00000000..e7468d57 --- /dev/null +++ b/allianceauth/timerboard/migrations/0007_alter_timer_structure.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.17 on 2025-01-06 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("timerboard", "0006_alter_timer_objective_alter_timer_structure_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="timer", + name="structure", + field=models.CharField( + choices=[ + ("POCO", "POCO"), + ("Orbital Skyhook", "Orbital Skyhook"), + ("I-HUB", "Soverreignty Hub"), + ("TCU", "TCU"), + ("POS[S]", "POS [S]"), + ("POS[M]", "POS [M]"), + ("POS[L]", "POS [L]"), + ("Astrahus", "Astrahus"), + ("Fortizar", "Fortizar"), + ("Keepstar", "Keepstar"), + ("Raitaru", "Raitaru"), + ("Azbel", "Azbel"), + ("Sotiyo", "Sotiyo"), + ("Athanor", "Athanor"), + ("Tatara", "Tatara"), + ("Pharolux Cyno Beacon", "Cyno Beacon"), + ("Tenebrex Cyno Jammer", "Cyno Jammer"), + ("Ansiblex Jump Gate", "Ansiblex Jump Gate"), + ("Mercenary Den", "Mercenary Den"), + ("Moon Mining Cycle", "Moon Mining Cycle"), + ("Metenox Moon Drill", "Metenox Moon Drill"), + ("Other", "Other"), + ], + default="Other", + max_length=254, + ), + ), + ] From d99f5858d83cc4daebb27996b9386c9fc664cef1 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 8 Jan 2025 23:04:29 +0100 Subject: [PATCH 056/178] [FIX] Moment.JS Localisation Load the right JS when our language code has 2 parts - fr_FR - it_IT - pl_PL And so on --- allianceauth/templates/bundles/moment-js.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/allianceauth/templates/bundles/moment-js.html b/allianceauth/templates/bundles/moment-js.html index a094b4be..802411b9 100644 --- a/allianceauth/templates/bundles/moment-js.html +++ b/allianceauth/templates/bundles/moment-js.html @@ -1,7 +1,19 @@ +{% load i18n %} + + {% if locale and LANGUAGE_CODE != 'en' %} - + {% get_current_language as LANGUAGE_CODE %} + {% get_language_info for LANGUAGE_CODE as lang %} + + {% if lang.code == 'zh-hans' %} + + + {% else %} + + + {% endif %} {% endif %} From 3efdb8f12b24f0ec089eac22c5a2c8f9f658cd21 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Fri, 10 Jan 2025 08:29:02 +0000 Subject: [PATCH 057/178] Docker/Baremetal Analytics --- allianceauth/analytics/admin.py | 7 +++--- .../0010_alter_analyticsidentifier_options.py | 17 ++++++++++++++ allianceauth/analytics/models.py | 22 ++++++++----------- allianceauth/analytics/tasks.py | 16 +++++++++----- allianceauth/analytics/tests/test_models.py | 15 ++----------- allianceauth/analytics/utils.py | 14 ++++++++++++ docs/features/core/analytics.md | 1 + 7 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 allianceauth/analytics/migrations/0010_alter_analyticsidentifier_options.py diff --git a/allianceauth/analytics/admin.py b/allianceauth/analytics/admin.py index 383bbbe2..497f2d7c 100644 --- a/allianceauth/analytics/admin.py +++ b/allianceauth/analytics/admin.py @@ -1,15 +1,16 @@ from django.contrib import admin from .models import AnalyticsIdentifier, AnalyticsTokens +from solo.admin import SingletonModelAdmin @admin.register(AnalyticsIdentifier) -class AnalyticsIdentifierAdmin(admin.ModelAdmin): +class AnalyticsIdentifierAdmin(SingletonModelAdmin): search_fields = ['identifier', ] - list_display = ('identifier',) + list_display = ['identifier', ] @admin.register(AnalyticsTokens) class AnalyticsTokensAdmin(admin.ModelAdmin): search_fields = ['name', ] - list_display = ('name', 'type',) + list_display = ['name', 'type', ] diff --git a/allianceauth/analytics/migrations/0010_alter_analyticsidentifier_options.py b/allianceauth/analytics/migrations/0010_alter_analyticsidentifier_options.py new file mode 100644 index 00000000..7c1993e9 --- /dev/null +++ b/allianceauth/analytics/migrations/0010_alter_analyticsidentifier_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-12-11 02:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('analytics', '0009_remove_analyticstokens_ignore_paths_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='analyticsidentifier', + options={'verbose_name': 'Analytics Identifier'}, + ), + ] diff --git a/allianceauth/analytics/models.py b/allianceauth/analytics/models.py index ddee495a..16ea0221 100644 --- a/allianceauth/analytics/models.py +++ b/allianceauth/analytics/models.py @@ -1,23 +1,19 @@ +from typing import Literal from django.db import models -from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ - +from solo.models import SingletonModel from uuid import uuid4 -class AnalyticsIdentifier(models.Model): +class AnalyticsIdentifier(SingletonModel): - identifier = models.UUIDField( - default=uuid4, - editable=False) + identifier = models.UUIDField(default=uuid4, editable=False) - def save(self, *args, **kwargs): - if not self.pk and AnalyticsIdentifier.objects.exists(): - # Force a single object - raise ValidationError('There is can be only one \ - AnalyticsIdentifier instance') - self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1 - return super().save(*args, **kwargs) + def __str__(self) -> Literal['Analytics Identifier']: + return "Analytics Identifier" + + class Meta: + verbose_name = "Analytics Identifier" class AnalyticsTokens(models.Model): diff --git a/allianceauth/analytics/tasks.py b/allianceauth/analytics/tasks.py index cc9ef160..d0a76c30 100644 --- a/allianceauth/analytics/tasks.py +++ b/allianceauth/analytics/tasks.py @@ -5,6 +5,7 @@ from django.apps import apps from celery import shared_task from .models import AnalyticsTokens, AnalyticsIdentifier from .utils import ( + existence_baremetal_or_docker, install_stat_addons, install_stat_tokens, install_stat_users) @@ -67,8 +68,8 @@ def analytics_event(namespace: str, value=value).apply_async(priority=9) -@shared_task() -def analytics_daily_stats(): +@shared_task +def analytics_daily_stats() -> None: """Celery Task: Do not call directly Gathers a series of daily statistics @@ -77,6 +78,7 @@ def analytics_daily_stats(): users = install_stat_users() tokens = install_stat_tokens() addons = install_stat_addons() + existence_type = existence_baremetal_or_docker() logger.debug("Running Daily Analytics Upload") analytics_event(namespace='allianceauth.analytics', @@ -84,6 +86,11 @@ def analytics_daily_stats(): label='existence', value=1, event_type='Stats') + analytics_event(namespace='allianceauth.analytics', + task='send_install_stats', + label=existence_type, + value=1, + event_type='Stats') analytics_event(namespace='allianceauth.analytics', task='send_install_stats', label='users', @@ -99,7 +106,6 @@ def analytics_daily_stats(): label='addons', value=addons, event_type='Stats') - for appconfig in apps.get_app_configs(): if appconfig.label in [ "django_celery_beat", @@ -135,7 +141,7 @@ def analytics_daily_stats(): event_type='Stats') -@shared_task() +@shared_task def send_ga_tracking_celery_event( measurement_id: str, secret: str, @@ -165,7 +171,7 @@ def send_ga_tracking_celery_event( } payload = { - 'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex, + 'client_id': AnalyticsIdentifier.get_solo().identifier.hex, "user_properties": { "allianceauth_version": { "value": __version__ diff --git a/allianceauth/analytics/tests/test_models.py b/allianceauth/analytics/tests/test_models.py index 452c4eaa..979f2720 100644 --- a/allianceauth/analytics/tests/test_models.py +++ b/allianceauth/analytics/tests/test_models.py @@ -1,9 +1,8 @@ from allianceauth.analytics.models import AnalyticsIdentifier -from django.core.exceptions import ValidationError from django.test.testcases import TestCase -from uuid import UUID, uuid4 +from uuid import uuid4 # Identifiers @@ -14,14 +13,4 @@ uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c" class TestAnalyticsIdentifier(TestCase): def test_identifier_random(self): - self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4) - - def test_identifier_singular(self): - AnalyticsIdentifier.objects.all().delete() - AnalyticsIdentifier.objects.create(identifier=uuid_1) - # Yeah i have multiple asserts here, they all do the same thing - with self.assertRaises(ValidationError): - AnalyticsIdentifier.objects.create(identifier=uuid_2) - self.assertEqual(AnalyticsIdentifier.objects.count(), 1) - self.assertEqual(AnalyticsIdentifier.objects.get( - pk=1).identifier, UUID(uuid_1)) + self.assertNotEqual(AnalyticsIdentifier.get_solo(), uuid4) diff --git a/allianceauth/analytics/utils.py b/allianceauth/analytics/utils.py index e8c57927..fa50084f 100644 --- a/allianceauth/analytics/utils.py +++ b/allianceauth/analytics/utils.py @@ -1,3 +1,4 @@ +import os from django.apps import apps from allianceauth.authentication.models import User from esi.models import Token @@ -34,3 +35,16 @@ def install_stat_addons() -> int: The Number of Installed Apps""" addons = len(list(apps.get_app_configs())) return addons + + +def existence_baremetal_or_docker() -> str: + """Checks the Installation Type of an install + + Returns + ------- + str + existence_baremetal or existence_docker""" + docker_tag = os.getenv('AA_DOCKER_TAG') + if docker_tag: + return "existence_docker" + return "existence_baremetal" diff --git a/docs/features/core/analytics.md b/docs/features/core/analytics.md index d610edb6..d04c47a3 100644 --- a/docs/features/core/analytics.md +++ b/docs/features/core/analytics.md @@ -27,6 +27,7 @@ Analytics comes preloaded with our Google Analytics token, and the three types o Our Daily Stats contain the following: - A phone-in task to identify a server's existence +- A phone-in task to identify if a server is Bare-Metal or Dockerized - A task to send the Number of User models - A task to send the Number of Token Models - A task to send the Number of Installed Apps From a03c7668400bfad8ba43e4040e2ddba3098a91df Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Fri, 10 Jan 2025 11:41:31 +0100 Subject: [PATCH 058/178] [FIX] Spelling mistakes Thx @soratidus999 --- .../timerboard/migrations/0007_alter_timer_structure.py | 2 +- allianceauth/timerboard/models.py | 2 +- allianceauth/timerboard/views.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/allianceauth/timerboard/migrations/0007_alter_timer_structure.py b/allianceauth/timerboard/migrations/0007_alter_timer_structure.py index e7468d57..26513513 100644 --- a/allianceauth/timerboard/migrations/0007_alter_timer_structure.py +++ b/allianceauth/timerboard/migrations/0007_alter_timer_structure.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): choices=[ ("POCO", "POCO"), ("Orbital Skyhook", "Orbital Skyhook"), - ("I-HUB", "Soverreignty Hub"), + ("I-HUB", "Sovereignty Hub"), ("TCU", "TCU"), ("POS[S]", "POS [S]"), ("POS[M]", "POS [M]"), diff --git a/allianceauth/timerboard/models.py b/allianceauth/timerboard/models.py index 6823ea8d..bf6f9b08 100644 --- a/allianceauth/timerboard/models.py +++ b/allianceauth/timerboard/models.py @@ -23,7 +23,7 @@ class Timer(models.Model): POCO = "POCO", _("POCO") ORBITALSKYHOOK = "Orbital Skyhook", _("Orbital Skyhook") - IHUB = "I-HUB", _("Soverreignty Hub") + IHUB = "I-HUB", _("Sovereignty Hub") TCU = "TCU", _("TCU") # Pending Remval POSS = "POS[S]", _("POS [S]") POSM = "POS[M]", _("POS [M]") diff --git a/allianceauth/timerboard/views.py b/allianceauth/timerboard/views.py index 27c6045f..0c7c76d1 100644 --- a/allianceauth/timerboard/views.py +++ b/allianceauth/timerboard/views.py @@ -54,7 +54,7 @@ class TimerView(BaseTimerView): if structure in bg_info: return "info" - elif structure in bg_waning: + elif structure in bg_warning: return "warning" elif structure in bg_danger: return "danger" @@ -84,7 +84,7 @@ class TimerView(BaseTimerView): Timer.Structure.POSM.value, # POS[M] Timer.Structure.POSL.value, # POS[L] ] - bg_waning = [ + bg_warning = [ Timer.Structure.ANSIBLEX.value, # Ansiblex Jump Gate Timer.Structure.ATHANOR.value, # Athanor Timer.Structure.AZBEL.value, # Azbel From 60998bffc2dd3b05815e402d9fb7dd4eb0f917c4 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Fri, 10 Jan 2025 12:10:49 +0000 Subject: [PATCH 059/178] Cron Offset Tasks --- allianceauth/apps.py | 1 - allianceauth/crontab/__init__.py | 3 + allianceauth/crontab/apps.py | 14 ++++ allianceauth/crontab/models.py | 23 ++++++ allianceauth/crontab/schedulers.py | 63 +++++++++++++++ allianceauth/crontab/tests/__init__.py | 0 allianceauth/crontab/tests/test_models.py | 63 +++++++++++++++ allianceauth/crontab/tests/test_utils.py | 80 +++++++++++++++++++ allianceauth/crontab/utils.py | 41 ++++++++++ .../project_name/settings/base.py | 6 +- docs/development/tech_docu/celery.md | 51 +++++++++++- 11 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 allianceauth/crontab/__init__.py create mode 100644 allianceauth/crontab/apps.py create mode 100644 allianceauth/crontab/models.py create mode 100644 allianceauth/crontab/schedulers.py create mode 100644 allianceauth/crontab/tests/__init__.py create mode 100644 allianceauth/crontab/tests/test_models.py create mode 100644 allianceauth/crontab/tests/test_utils.py create mode 100644 allianceauth/crontab/utils.py diff --git a/allianceauth/apps.py b/allianceauth/apps.py index 098f50ba..1191fed9 100644 --- a/allianceauth/apps.py +++ b/allianceauth/apps.py @@ -1,5 +1,4 @@ from django.apps import AppConfig -from django.core.checks import Warning, Error, register class AllianceAuthConfig(AppConfig): diff --git a/allianceauth/crontab/__init__.py b/allianceauth/crontab/__init__.py new file mode 100644 index 00000000..c621f6d2 --- /dev/null +++ b/allianceauth/crontab/__init__.py @@ -0,0 +1,3 @@ +""" +Alliance Auth Crontab Utilities +""" diff --git a/allianceauth/crontab/apps.py b/allianceauth/crontab/apps.py new file mode 100644 index 00000000..e47e39ed --- /dev/null +++ b/allianceauth/crontab/apps.py @@ -0,0 +1,14 @@ +""" +Crontab App Config +""" + +from django.apps import AppConfig + + +class CrontabConfig(AppConfig): + """ + Crontab App Config + """ + + name = "allianceauth.crontab" + label = "crontab" diff --git a/allianceauth/crontab/models.py b/allianceauth/crontab/models.py new file mode 100644 index 00000000..f23b35ea --- /dev/null +++ b/allianceauth/crontab/models.py @@ -0,0 +1,23 @@ +from random import random +from django.db import models +from django.utils.translation import gettext_lazy as _ +from solo.models import SingletonModel + + +def random_default() -> float: + return random() + + +class CronOffset(SingletonModel): + + minute = models.FloatField(_("Minute Offset"), default=random_default) + hour = models.FloatField(_("Hour Offset"), default=random_default) + day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default) + month_of_year = models.FloatField(_("Month of Year Offset"), default=random_default) + day_of_week = models.FloatField(_("Day of Week Offset"), default=random_default) + + def __str__(self) -> str: + return "Cron Offsets" + + class Meta: + verbose_name = "Cron Offsets" diff --git a/allianceauth/crontab/schedulers.py b/allianceauth/crontab/schedulers.py new file mode 100644 index 00000000..0b36eead --- /dev/null +++ b/allianceauth/crontab/schedulers.py @@ -0,0 +1,63 @@ +from django.core.exceptions import ObjectDoesNotExist +from django_celery_beat.schedulers import ( + DatabaseScheduler +) +from django_celery_beat.models import CrontabSchedule +from django.db.utils import OperationalError, ProgrammingError + +from celery import schedules +from celery.utils.log import get_logger + +from allianceauth.crontab.models import CronOffset +from allianceauth.crontab.utils import offset_cron + +logger = get_logger(__name__) + + +class OffsetDatabaseScheduler(DatabaseScheduler): + """ + Customization of Django Celery Beat, Database Scheduler + Takes the Celery Schedule from local.py and applies our AA Framework Cron Offset, if apply_offset is true + Otherwise it passes it through as normal + """ + def update_from_dict(self, mapping): + s = {} + + try: + cron_offset = CronOffset.get_solo() + except (OperationalError, ProgrammingError, ObjectDoesNotExist) as exc: + # This is just incase we haven't migrated yet or something + logger.warning( + "OffsetDatabaseScheduler: Could not fetch CronOffset (%r). " + "Defering to DatabaseScheduler", + exc + ) + return super().update_from_dict(mapping) + + for name, entry_fields in mapping.items(): + try: + apply_offset = entry_fields.pop("apply_offset", False) + entry = self.Entry.from_entry(name, app=self.app, **entry_fields) + + if entry.model.enabled and apply_offset: + schedule_obj = entry.schedule + if isinstance(schedule_obj, schedules.crontab): + offset_cs = CrontabSchedule.from_schedule(offset_cron(schedule_obj)) + offset_cs, created = CrontabSchedule.objects.get_or_create( + minute=offset_cs.minute, + hour=offset_cs.hour, + day_of_month=offset_cs.day_of_month, + month_of_year=offset_cs.month_of_year, + day_of_week=offset_cs.day_of_week, + timezone=offset_cs.timezone, + ) + entry.model.crontab = offset_cs + entry.model.save() + logger.debug(f"Offset applied for '{name}' due to 'apply_offset' = True.") + + s[name] = entry + + except Exception as e: + logger.exception("Error updating schedule for %s: %r", name, e) + + self.schedule.update(s) diff --git a/allianceauth/crontab/tests/__init__.py b/allianceauth/crontab/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/crontab/tests/test_models.py b/allianceauth/crontab/tests/test_models.py new file mode 100644 index 00000000..cb92e667 --- /dev/null +++ b/allianceauth/crontab/tests/test_models.py @@ -0,0 +1,63 @@ +from unittest.mock import patch +from django.test import TestCase + +from allianceauth.crontab.models import CronOffset + + +class CronOffsetModelTest(TestCase): + def test_cron_offset_is_singleton(self): + """ + Test that CronOffset is indeed a singleton and that + multiple calls to get_solo() return the same instance. + """ + offset1 = CronOffset.get_solo() + offset2 = CronOffset.get_solo() + + # They should be the exact same object in memory + self.assertEqual(offset1.pk, offset2.pk) + + def test_default_values_random(self): + """ + Test that the default values are set via random_default() when + no explicit value is provided. We'll patch 'random.random' to + produce predictable output. + """ + with patch('allianceauth.crontab.models.random', return_value=0.1234): + # Force creation of a new CronOffset by clearing the existing one + CronOffset.objects.all().delete() + + offset = CronOffset.get_solo() # This triggers creation + + # All fields should be 0.1234, because we patched random() + self.assertAlmostEqual(offset.minute, 0.1234) + self.assertAlmostEqual(offset.hour, 0.1234) + self.assertAlmostEqual(offset.day_of_month, 0.1234) + self.assertAlmostEqual(offset.month_of_year, 0.1234) + self.assertAlmostEqual(offset.day_of_week, 0.1234) + + def test_update_offset_values(self): + """ + Test that we can update the offsets and retrieve them. + """ + offset = CronOffset.get_solo() + offset.minute = 0.5 + offset.hour = 0.25 + offset.day_of_month = 0.75 + offset.month_of_year = 0.99 + offset.day_of_week = 0.33 + offset.save() + + # Retrieve again to ensure changes persist + saved_offset = CronOffset.get_solo() + self.assertEqual(saved_offset.minute, 0.5) + self.assertEqual(saved_offset.hour, 0.25) + self.assertEqual(saved_offset.day_of_month, 0.75) + self.assertEqual(saved_offset.month_of_year, 0.99) + self.assertEqual(saved_offset.day_of_week, 0.33) + + def test_str_representation(self): + """ + Verify the __str__ method returns 'Cron Offsets'. + """ + offset = CronOffset.get_solo() + self.assertEqual(str(offset), "Cron Offsets") diff --git a/allianceauth/crontab/tests/test_utils.py b/allianceauth/crontab/tests/test_utils.py new file mode 100644 index 00000000..48592c99 --- /dev/null +++ b/allianceauth/crontab/tests/test_utils.py @@ -0,0 +1,80 @@ +# myapp/tests/test_tasks.py + +import logging +from unittest.mock import patch +from django.test import TestCase +from django.db import ProgrammingError +from celery.schedules import crontab + +from allianceauth.crontab.utils import offset_cron +from allianceauth.crontab.models import CronOffset + +logger = logging.getLogger(__name__) + + +class TestOffsetCron(TestCase): + + def test_offset_cron_normal(self): + """ + Test that offset_cron modifies the minute/hour fields + based on the CronOffset values when everything is normal. + """ + # We'll create a mock CronOffset instance + mock_offset = CronOffset(minute=0.5, hour=0.5) + + # Our initial crontab schedule + original_schedule = crontab( + minute=[0, 5, 55], + hour=[0, 3, 23], + day_of_month='*', + month_of_year='*', + day_of_week='*' + ) + + # Patch CronOffset.get_solo to return our mock offset + with patch('allianceauth.crontab.models.CronOffset.get_solo', return_value=mock_offset): + new_schedule = offset_cron(original_schedule) + + # Check the new minute/hour + # minute 0 -> 0 + round(60 * 0.5) = 30 % 60 = 30 + # minute 5 -> 5 + 30 = 35 % 60 = 35 + # minute 55 -> 55 + 30 = 85 % 60 = 25 --> sorted => 25,30,35 + self.assertEqual(new_schedule._orig_minute, '25,30,35') + + # hour 0 -> 0 + round(24 * 0.5) = 12 % 24 = 12 + # hour 3 -> 3 + 12 = 15 % 24 = 15 + # hour 23 -> 23 + 12 = 35 % 24 = 11 --> sorted => 11,12,15 + self.assertEqual(new_schedule._orig_hour, '11,12,15') + + # Check that other fields are unchanged + self.assertEqual(new_schedule._orig_day_of_month, '*') + self.assertEqual(new_schedule._orig_month_of_year, '*') + self.assertEqual(new_schedule._orig_day_of_week, '*') + + def test_offset_cron_programming_error(self): + """ + Test that if a ProgrammingError is raised (e.g. before migrations), + offset_cron just returns the original schedule. + """ + original_schedule = crontab(minute=[0, 15, 30], hour=[1, 2, 3]) + + # Force get_solo to raise ProgrammingError + with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=ProgrammingError()): + new_schedule = offset_cron(original_schedule) + + # Should return the original schedule unchanged + self.assertEqual(new_schedule, original_schedule) + + def test_offset_cron_unexpected_exception(self): + """ + Test that if any other exception is raised, offset_cron + also returns the original schedule, and logs the error. + """ + original_schedule = crontab(minute='0', hour='0') + + # Force get_solo to raise a generic Exception + with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=Exception("Something bad")): + new_schedule = offset_cron(original_schedule) + + # Should return the original schedule unchanged + self.assertEqual(new_schedule, original_schedule) diff --git a/allianceauth/crontab/utils.py b/allianceauth/crontab/utils.py new file mode 100644 index 00000000..150664ca --- /dev/null +++ b/allianceauth/crontab/utils.py @@ -0,0 +1,41 @@ +from celery.schedules import crontab +import logging +from allianceauth.crontab.models import CronOffset +from django.db import ProgrammingError + + +logger = logging.getLogger(__name__) + + +def offset_cron(schedule: crontab) -> crontab: + """Take a crontab and apply a series of precalculated offsets to spread out tasks execution on remote resources + + Args: + schedule (crontab): celery.schedules.crontab() + + Returns: + crontab: A crontab with offsetted Minute and Hour fields + """ + + try: + cron_offset = CronOffset.get_solo() + new_minute = [(m + (round(60 * cron_offset.minute))) % 60 for m in schedule.minute] + new_hour = [(m + (round(24 * cron_offset.hour))) % 24 for m in schedule.hour] + + return crontab( + minute=",".join(str(m) for m in sorted(new_minute)), + hour=",".join(str(h) for h in sorted(new_hour)), + day_of_month=schedule._orig_day_of_month, + month_of_year=schedule._orig_month_of_year, + day_of_week=schedule._orig_day_of_week) + + except ProgrammingError as e: + # If this is called before migrations are run hand back the default schedule + # These offsets are stored in a Singleton Model, + logger.error(e) + return schedule + + except Exception as e: + # We absolutely cant fail to hand back a schedule + logger.error(e) + return schedule diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py index 9e8d6957..56e7c493 100644 --- a/allianceauth/project_template/project_name/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ 'allianceauth.theme.flatly', 'allianceauth.theme.materia', "allianceauth.custom_css", + 'allianceauth.crontab', 'sri', ] @@ -51,7 +52,7 @@ SECRET_KEY = "wow I'm a really bad default secret key" # Celery configuration BROKER_URL = 'redis://localhost:6379/0' -CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" +CELERYBEAT_SCHEDULER = "allianceauth.crontab.schedulers.OffsetDatabaseScheduler" CELERYBEAT_SCHEDULE = { 'esi_cleanup_callbackredirect': { 'task': 'esi.tasks.cleanup_callbackredirect', @@ -64,10 +65,12 @@ CELERYBEAT_SCHEDULE = { 'run_model_update': { 'task': 'allianceauth.eveonline.tasks.run_model_update', 'schedule': crontab(minute='0', hour="*/6"), + 'apply_offset': True }, 'check_all_character_ownership': { 'task': 'allianceauth.authentication.tasks.check_all_character_ownership', 'schedule': crontab(minute='0', hour='*/4'), + 'apply_offset': True }, 'analytics_daily_stats': { 'task': 'allianceauth.analytics.tasks.analytics_daily_stats', @@ -75,6 +78,7 @@ CELERYBEAT_SCHEDULE = { } } + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(PROJECT_DIR) diff --git a/docs/development/tech_docu/celery.md b/docs/development/tech_docu/celery.md index 3f39d299..b9571170 100644 --- a/docs/development/tech_docu/celery.md +++ b/docs/development/tech_docu/celery.md @@ -123,6 +123,7 @@ Example setting: CELERYBEAT_SCHEDULE['structures_update_all_structures'] = { 'task': 'structures.tasks.update_all_structures', 'schedule': crontab(minute='*/30'), + 'apply_offset': True, } ``` @@ -130,6 +131,7 @@ CELERYBEAT_SCHEDULE['structures_update_all_structures'] = { - `'task'`: Name of your task (full path) - `'schedule'`: Schedule definition (see Celery documentation on [Periodic Tasks](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html) for details) +- `'apply_offset'`: Boolean, Apply a Delay unique to the install, in order to reduce impact on ESI. See [Apply Offset](#apply-offset) ## How can I use priorities for tasks? @@ -174,9 +176,54 @@ Large numbers of installs running the same crontab (ie. `0 * * * *`) can all sla Consider Artificially smoothing out your tasks with a few methods -### Offset Crontabs +### Apply Offset -Avoid running your tasks on the hour or other nice neat human numbers, consider 23 minutes on the hour instead of at zero (`28 * * * *`) +`allianceauth.crontab` contains a series of Offsets stored in the DB that are both static for an install, but random across all AA installs. + +This enables us to spread our load on ESI (or other external resources) across a greater window, making it unlikely that two installs will hit ESI at the same time. + +Tasks defined in local.py, can have `'apply_offset': True` added to their Task Definition + +```python +CELERYBEAT_SCHEDULE['taskname'] = { + 'task': 'module.tasks.task', + 'schedule': crontab(minute='*/30'), + 'apply_offset': True, +} +``` + +Tasks added to directly to Django Celery Beat Models (Using a Management Task etc) can pass their Cron Schedule through offset_cron(crontab) + +```{eval-rst} +.. automodule:: allianceauth.crontab.utils + :members: + :undoc-members: +``` + +```python +from django_celery_beat.models import CrontabSchedule, PeriodicTask +from celery.schedules import crontab + +schedule = CrontabSchedule.from_schedule(offset_cron(crontab(minute='0', hour='0'))) + +schedule, created = CrontabSchedule.objects.get_or_create( + minute=schedule.minute, + hour=schedule.hour, + day_of_month=schedule.day_of_month, + month_of_year=schedule.month_of_year, + day_of_week=schedule.day_of_week, + timezone=schedule.timezone, +) + +PeriodicTask.objects.update_or_create( + task='module.tasks.task', + defaults={ + 'crontab': schedule, + 'name': 'task name', + 'enabled': True + } +) +``` ### Subset Tasks From 92c2af9975c9b6ca9f8bfba705368ffd8ddb0fb9 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sun, 12 Jan 2025 17:12:23 +0100 Subject: [PATCH 060/178] [CHANGE] Timertables to Datatables --- .../templates/timerboard/timertable.html | 146 +++++++++--------- .../timerboard/templates/timerboard/view.html | 27 +++- 2 files changed, 98 insertions(+), 75 deletions(-) diff --git a/allianceauth/timerboard/templates/timerboard/timertable.html b/allianceauth/timerboard/templates/timerboard/timertable.html index 014b01e8..13690dc2 100644 --- a/allianceauth/timerboard/templates/timerboard/timertable.html +++ b/allianceauth/timerboard/templates/timerboard/timertable.html @@ -2,80 +2,84 @@ {% load i18n %}
    - - - - - - - - - - - {% if perms.auth.timer_management %} - - {% endif %} - - - {% for timer in timers %} - - - - - - - - - - - - - - +
    {% translate "Details" %}{% translate "Objective" %}{% translate "System" %}{% translate "Structure" %}{% translate "Eve Time" %}{% translate "Local Time" %}{% translate "Creator" %}{% translate "Action" %}
    - {{ timer.details }} - - {% if timer.timer_type != 'UNSPECIFIED' %} -
    - ({{ timer.get_timer_type_display }}) - {% endif %} -
    - {% comment %} Objective: Hostile (BG: Danger) {% endcomment %} - {% if timer.objective == "Hostile" %} -
    - - {% comment %} Objective: Friendly (BG: Primare) {% endcomment %} - {% elif timer.objective == "Friendly" %} -
    - - {% comment %} Objective: Neutral (BG: Secondary) {% endcomment %} - {% elif timer.objective == "Neutral" %} -
    - {% endif %} - - {{ timer.get_objective_display }} -
    -
    - {{ timer.system }} {{ timer.planet_moon }} - -
    - {{ timer.get_structure_display }} -
    -
    {{ timer.eve_time | date:"Y-m-d H:i" }} -
    -
    -
    {{ timer.eve_character.character_name }}
    + + + + + + + + + {% if perms.auth.timer_management %} - + {% endif %} - {% endfor %} + + + + {% for timer in timers %} + + + + + + + + + + + + + + + + {% if perms.auth.timer_management %} + + {% endif %} + + {% endfor %} +
    {% translate "Details" %}{% translate "Objective" %}{% translate "System" %}{% translate "Structure" %}{% translate "Eve Time" %}{% translate "Local Time" %}{% translate "Creator" %} - - - - - - - {% translate "Action" %}
    + {{ timer.details }} + + {% if timer.timer_type != 'UNSPECIFIED' %} +
    + ({{ timer.get_timer_type_display }}) + {% endif %} +
    + {% comment %} Objective: Hostile (BG: Danger) {% endcomment %} + {% if timer.objective == "Hostile" %} +
    + + {% comment %} Objective: Friendly (BG: Primare) {% endcomment %} + {% elif timer.objective == "Friendly" %} +
    + + {% comment %} Objective: Neutral (BG: Secondary) {% endcomment %} + {% elif timer.objective == "Neutral" %} +
    + {% endif %} + + {{ timer.get_objective_display }} +
    +
    + {{ timer.system }} {{ timer.planet_moon }} + +
    + {{ timer.get_structure_display }} +
    +
    {{ timer.eve_time | date:"Y-m-d H:i" }} +
    +
    +
    {{ timer.eve_character.character_name }} + + + + + + +
    diff --git a/allianceauth/timerboard/templates/timerboard/view.html b/allianceauth/timerboard/templates/timerboard/view.html index 85948f20..a39fc027 100644 --- a/allianceauth/timerboard/templates/timerboard/view.html +++ b/allianceauth/timerboard/templates/timerboard/view.html @@ -25,7 +25,7 @@ {% endblock header_nav_collapse_right %} {% block content %} -
    +
    {% translate "Current Eve Time:" %} @@ -42,7 +42,7 @@
    - {% include "timerboard/timertable.html" with timers=corp_timers %} + {% include "timerboard/timertable.html" with id="corp-timers" timers=corp_timers %}
    {% endif %} @@ -56,7 +56,7 @@
    {% if future_timers %} - {% include "timerboard/timertable.html" with timers=future_timers %} + {% include "timerboard/timertable.html" with id="future-timers" timers=future_timers %} {% else %}
    {% translate "No upcoming timers." %} @@ -74,7 +74,7 @@
    {% if past_timers %} - {% include "timerboard/timertable.html" with timers=past_timers %} + {% include "timerboard/timertable.html" with id="past-timers" timers=past_timers %} {% else %}
    {% translate "No past timers." %} @@ -85,9 +85,14 @@
    {% endblock content %} +{% block extra_css %} + {% include "bundles/datatables-css-bs5.html" %} +{% endblock extra_css %} + {% block extra_javascript %} {% include "bundles/moment-js.html" with locale=True %} {% include "bundles/timers-js.html" %} + {% include "bundles/datatables-js-bs5.html" %} {% endblock extra_javascript %} From 856e939c212bd1e27ef16c9969de7604f1a33a29 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Mon, 20 Jan 2025 04:35:10 +0100 Subject: [PATCH 061/178] [ADD] Locale mapping for DataTables and Moment.JS --- .pre-commit-config.yaml | 13 +- .../templates/authentication/tokens.html | 12 +- .../templates/corputils/corpstats.html | 6 + .../corputils/templates/corputils/search.html | 4 + .../templates/groupmanagement/audit.html | 4 + .../groupmanagement/groupmembers.html | 4 + .../templates/groupmanagement/groups.html | 7 +- .../templates/permissions_tool/audit.html | 4 + .../templates/permissions_tool/overview.html | 4 + .../project_name/settings/base.py | 33 +++ .../mumble/mumble_connection_history.html | 5 + allianceauth/srp/templates/srp/data.html | 4 + .../DataTables/Plugins/2.2.1/i18n/cs.json | 247 ++++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/de-DE.json | 243 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/es-ES.json | 244 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/fr-FR.json | 245 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/it-IT.json | 244 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/ja.json | 84 ++++++ .../DataTables/Plugins/2.2.1/i18n/ko.json | 122 +++++++++ .../DataTables/Plugins/2.2.1/i18n/nl-NL.json | 246 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/pl.json | 240 +++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/ru.json | 247 ++++++++++++++++++ .../DataTables/Plugins/2.2.1/i18n/uk.json | 186 +++++++++++++ .../Plugins/2.2.1/i18n/zh-HANT.json | 240 +++++++++++++++++ .../libs/moment.js/2.29.4/locale/cs.js | 191 ++++++++++++++ .../libs/moment.js/2.29.4/locale/de.js | 89 +++++++ .../libs/moment.js/2.29.4/locale/es.js | 121 +++++++++ .../libs/moment.js/2.29.4/locale/fr.js | 119 +++++++++ .../libs/moment.js/2.29.4/locale/it.js | 117 +++++++++ .../libs/moment.js/2.29.4/locale/ja.js | 159 +++++++++++ .../libs/moment.js/2.29.4/locale/ko.js | 86 ++++++ .../libs/moment.js/2.29.4/locale/nl.js | 115 ++++++++ .../libs/moment.js/2.29.4/locale/pl.js | 151 +++++++++++ .../libs/moment.js/2.29.4/locale/ru.js | 224 ++++++++++++++++ .../libs/moment.js/2.29.4/locale/uk.js | 178 +++++++++++++ .../libs/moment.js/2.29.4/locale/zh-cn.js | 131 ++++++++++ allianceauth/templates/bundles/moment-js.html | 15 +- allianceauth/templatetags/aa_i18n.py | 90 +++++++ .../timerboard/templates/timerboard/view.html | 7 +- 39 files changed, 4459 insertions(+), 22 deletions(-) create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/cs.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/de-DE.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/es-ES.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/fr-FR.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/it-IT.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/ja.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/ko.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/nl-NL.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/pl.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/ru.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/uk.json create mode 100644 allianceauth/static/allianceauth/libs/DataTables/Plugins/2.2.1/i18n/zh-HANT.json create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/cs.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/de.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/es.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/fr.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/it.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/ja.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/ko.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/nl.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/pl.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/ru.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/uk.js create mode 100644 allianceauth/static/allianceauth/libs/moment.js/2.29.4/locale/zh-cn.js create mode 100644 allianceauth/templatetags/aa_i18n.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04c74b56..91268822 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,9 +33,9 @@ repos: - id: detect-private-key - id: check-case-conflict # Python checks - # - id: check-docstring-first +# - id: check-docstring-first - id: debug-statements - # - id: requirements-txt-fixer +# - id: requirements-txt-fixer - id: fix-encoding-pragma args: [--remove] - id: fix-byte-order-marker @@ -50,7 +50,8 @@ repos: \.min\.js| \.po| \.mo| - swagger\.json + swagger\.json| + static/(.*)/libs/ ) - id: check-executables-have-shebangs - id: end-of-file-fixer @@ -60,7 +61,8 @@ repos: \.min\.js| \.po| \.mo| - swagger\.json + swagger\.json| + static/(.*)/libs/ ) - repo: https://github.com/editorconfig-checker/editorconfig-checker.python rev: 2.7.3 @@ -72,7 +74,8 @@ repos: allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less| \.po| \.mo| - swagger\.json + swagger\.json| + static/(.*)/libs/ ) - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.41.0 diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index 42e3c61b..4453efd7 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -1,5 +1,6 @@ {% extends "allianceauth/base-bs5.html" %} +{% load aa_i18n %} {% load i18n %} {% block page_title %} @@ -50,20 +51,23 @@ {% block extra_javascript %} {% include "bundles/datatables-js-bs5.html" %} + {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} + {% endblock %} diff --git a/allianceauth/permissions_tool/templates/permissions_tool/audit.html b/allianceauth/permissions_tool/templates/permissions_tool/audit.html index 3aa50e63..5055dc03 100644 --- a/allianceauth/permissions_tool/templates/permissions_tool/audit.html +++ b/allianceauth/permissions_tool/templates/permissions_tool/audit.html @@ -1,5 +1,6 @@ {% extends "allianceauth/base-bs5.html" %} +{% load aa_i18n %} {% load i18n %} {% block page_title %} @@ -57,11 +58,14 @@ {% include "bundles/datatables-js-bs5.html" %} {% include "bundles/filterdropdown-js.html" %} + {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} + {% if locale and LANGUAGE_CODE != 'en' %} - - {% get_current_language as LANGUAGE_CODE %} - {% get_language_info for LANGUAGE_CODE as lang %} + + {% get_momentjs_language_static LANGUAGE_CODE as MOMENTJS_LANG_PATH %} - {% if lang.code == 'zh-hans' %} - - - {% else %} - - - {% endif %} + {% endif %} diff --git a/allianceauth/templatetags/aa_i18n.py b/allianceauth/templatetags/aa_i18n.py new file mode 100644 index 00000000..216eca6b --- /dev/null +++ b/allianceauth/templatetags/aa_i18n.py @@ -0,0 +1,90 @@ +""" +Template tags for language mapping +""" + +from django.conf import settings +from django.template.defaulttags import register +from django.templatetags.static import static + + +@register.simple_tag +def get_datatable_language_code(language: str) -> str: + """ + Get the correct language code for DataTables + + :param language: Django's language code + :type language: str + :return: Mapped language code + :rtype: str + """ + + mapped_language = ( + settings.LANGUAGE_MAPPING["DataTables"].get(language) + if language != "en" + else "" + ) + + return mapped_language + + +@register.simple_tag +def get_momentjs_language_code(language: str) -> str: + """ + Get the correct language code for Moment.JS + + :param language: Django's language code + :type language: str + :return: Mapped language code + :rtype: str + """ + + mapped_language = ( + settings.LANGUAGE_MAPPING["MomentJS"].get(language) if language != "en" else "" + ) + + return mapped_language + + +@register.simple_tag +def get_datatables_language_static(language: str) -> str: + """ + Get the correct language code URL for DataTables + + :param language: Django's language code + :type language: str + :return: Mapped language code + :rtype: str + """ + + mapped_language = get_datatable_language_code(language) + static_url = ( + static( + path=f"allianceauth/libs/DataTables/Plugins/2.2.1/i18n/{mapped_language}.json" + ) + if mapped_language + else "" + ) + + return static_url + + +@register.simple_tag +def get_momentjs_language_static(language: str) -> str: + """ + Get the correct language code URL for Moment.JS + + :param language: Django's language code + :type language: str + :return: Mapped language code + :rtype: str + """ + + mapped_language = get_momentjs_language_code(language) + + static_url = ( + static(path=f"allianceauth/libs/moment.js/2.29.4/locale/{mapped_language}.js") + if mapped_language + else "" + ) + + return static_url diff --git a/allianceauth/timerboard/templates/timerboard/view.html b/allianceauth/timerboard/templates/timerboard/view.html index a39fc027..8746eb6c 100644 --- a/allianceauth/timerboard/templates/timerboard/view.html +++ b/allianceauth/timerboard/templates/timerboard/view.html @@ -1,5 +1,6 @@ {% extends "allianceauth/base-bs5.html" %} +{% load aa_i18n %} {% load i18n %} {% load evelinks %} @@ -94,6 +95,8 @@ {% include "bundles/timers-js.html" %} {% include "bundles/datatables-js-bs5.html" %} + {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} + From 43906f41b3dd865b54b7fcd3c7051460a3e0e171 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 1 Feb 2025 16:00:03 +0100 Subject: [PATCH 070/178] [FIX] Timerboard initial sort order --- allianceauth/timerboard/templates/timerboard/view.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/allianceauth/timerboard/templates/timerboard/view.html b/allianceauth/timerboard/templates/timerboard/view.html index 8746eb6c..8f473340 100644 --- a/allianceauth/timerboard/templates/timerboard/view.html +++ b/allianceauth/timerboard/templates/timerboard/view.html @@ -180,6 +180,9 @@ $(document).ready(() => { const dtOptions = { language: {url: '{{ DT_LANG_PATH }}'}, + order: [ + [4, 'asc'] + ], }; {% if perms.auth.timer_management %} From f047943eb7ae1933ee13a4cb2f0fdc658bbfe674 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 1 Feb 2025 16:11:44 +0100 Subject: [PATCH 071/178] [CHANGE] Define language versions --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91268822..3f2e7f21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,11 @@ # Update this file: # pre-commit autoupdate +# Set the default language versions for the hooks +default_language_version: + python: python3 # Force all Python hooks to use Python 3 + node: 22.12.0 # Force all Node hooks to use Node 22.12.0 + repos: # Code Upgrades - repo: https://github.com/asottile/pyupgrade @@ -81,6 +86,7 @@ repos: rev: v0.41.0 hooks: - id: markdownlint + language: node args: - --disable=MD013 # Infrastructure From 8be2760fc4c31ac733e71eae2908a35816d575fb Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 1 Feb 2025 16:14:36 +0100 Subject: [PATCH 072/178] [CHANGNE] Update repos and dependencies --- .pre-commit-config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f2e7f21..5362b10d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,19 +11,19 @@ default_language_version: repos: # Code Upgrades - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.17.0 + rev: 1.22.2 hooks: - id: django-upgrade args: [--target-version=4.2] # Formatting - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: # Identify invalid files - id: check-ast @@ -70,7 +70,7 @@ repos: static/(.*)/libs/ ) - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 2.7.3 + rev: 3.2.0 hooks: - id: editorconfig-checker exclude: | @@ -83,7 +83,7 @@ repos: static/(.*)/libs/ ) - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.41.0 + rev: v0.44.0 hooks: - id: markdownlint language: node @@ -91,7 +91,7 @@ repos: - --disable=MD013 # Infrastructure - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.1.3 + rev: v2.5.0 hooks: - id: pyproject-fmt name: pyproject.toml formatter @@ -99,9 +99,9 @@ repos: args: - --indent=4 additional_dependencies: - - tox==4.15.0 # https://github.com/tox-dev/tox/releases/latest + - tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.23 hooks: - id: validate-pyproject name: Validate pyproject.toml From b5ad1c8a1a3ccef6e665517c80702a3abc4ba082 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 1 Feb 2025 16:18:08 +0100 Subject: [PATCH 073/178] [CHANGE] Use global exclude instead of per hook --- .pre-commit-config.yaml | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5362b10d..51c69b69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,20 @@ default_language_version: python: python3 # Force all Python hooks to use Python 3 node: 22.12.0 # Force all Node hooks to use Node 22.12.0 +# Globally exclude files +# https://pre-commit.com/#top_level-exclude +exclude: | + (?x)( + LICENSE| + allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less| + \.min\.css| + \.min\.js| + \.po| + \.mo| + swagger\.json| + static/(.*)/libs/ + ) + repos: # Code Upgrades - repo: https://github.com/asottile/pyupgrade @@ -49,39 +63,12 @@ repos: args: [--fix=lf] - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - exclude: | - (?x)( - \.min\.css| - \.min\.js| - \.po| - \.mo| - swagger\.json| - static/(.*)/libs/ - ) - id: check-executables-have-shebangs - id: end-of-file-fixer - exclude: | - (?x)( - \.min\.css| - \.min\.js| - \.po| - \.mo| - swagger\.json| - static/(.*)/libs/ - ) - repo: https://github.com/editorconfig-checker/editorconfig-checker.python rev: 3.2.0 hooks: - id: editorconfig-checker - exclude: | - (?x)( - LICENSE| - allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less| - \.po| - \.mo| - swagger\.json| - static/(.*)/libs/ - ) - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.44.0 hooks: From c2ae680f72f5cfe5c97bd13f91cc540cbb9a8cb9 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 1 Feb 2025 16:22:36 +0100 Subject: [PATCH 074/178] [CHANGE] Apply new pre-commit config --- pyproject.toml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ed04b9c..c6f73359 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "flit_core.buildapi" requires = [ - "flit-core<4,>=3.2", + "flit-core>=3.2,<4", ] [project] @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] @@ -41,15 +42,15 @@ dynamic = [ dependencies = [ "bcrypt", "beautifulsoup4", - "celery<6,>=5.2", + "celery>=5.2,<6", "celery-once>=3.0.1", - "django<5,>=4.2", + "django>=4.2,<5", "django-bootstrap-form", "django-bootstrap5>=23.3", "django-celery-beat>=2.3", "django-esi>=5", "django-redis>=5.2", - "django-registration<3.4,>=3.3", + "django-registration>=3.3,<3.4", "django-solo", "django-sortedm2m", "django-sri", @@ -70,7 +71,7 @@ optional-dependencies.docs = [ "myst-parser", "sphinx", "sphinx-copybutton", - "sphinx-rtd-theme<3,>=2", + "sphinx-rtd-theme>=2,<3", "sphinx-tabs", "sphinxcontrib-django", ] From 146c4c8d94e31c972439d667a9b99fc7e81441a5 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sun, 9 Feb 2025 08:55:51 +0100 Subject: [PATCH 075/178] [CHANGE] Update URL to apps management --- .../authentication/templates/authentication/tokens.html | 2 +- allianceauth/authentication/templates/public/middle_box.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index 4453efd7..fc9b9c35 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -14,7 +14,7 @@ {% block content %}

    - {% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %} + {% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://developers.eveonline.com/authorized-apps where possible."|urlize %}

    diff --git a/allianceauth/authentication/templates/public/middle_box.html b/allianceauth/authentication/templates/public/middle_box.html index cfc089e4..c6ad452f 100644 --- a/allianceauth/authentication/templates/public/middle_box.html +++ b/allianceauth/authentication/templates/public/middle_box.html @@ -29,7 +29,7 @@

    - + {% translate "Manage ESI Applications" %}

    From f497c18e5b5f6cd504ff57913a88beb5818ffdcb Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sun, 16 Feb 2025 23:20:36 +0100 Subject: [PATCH 076/178] [REMOVE] Unused imports --- allianceauth/analytics/tests/test_utils.py | 3 +-- allianceauth/authentication/decorators.py | 3 --- allianceauth/authentication/models.py | 1 - allianceauth/authentication/tests/__init__.py | 6 ------ allianceauth/authentication/tests/test_middleware.py | 1 - allianceauth/authentication/tests/test_signals.py | 8 +------- allianceauth/authentication/views.py | 1 - allianceauth/crontab/tests/test_utils.py | 2 -- allianceauth/custom_css/widgets.py | 3 --- allianceauth/eveonline/autogroups/tests/test_models.py | 1 - allianceauth/eveonline/evelinks/tests/test_evelinks.py | 2 -- allianceauth/fleetactivitytracking/views.py | 2 +- .../menu/tests/templatetags/test_menu_menu_items.py | 2 +- allianceauth/notifications/tests/test_templatetags.py | 2 +- allianceauth/notifications/tests/test_views.py | 2 +- allianceauth/permissions_tool/views.py | 2 +- allianceauth/services/hooks.py | 7 ------- .../services/modules/discord/tests/piloting_tasks.py | 1 - allianceauth/services/modules/discourse/manager.py | 1 - allianceauth/services/modules/discourse/views.py | 2 +- allianceauth/services/modules/ips4/tasks.py | 1 - allianceauth/tests/test_auth_utils.py | 6 ------ allianceauth/urls.py | 1 - 23 files changed, 8 insertions(+), 52 deletions(-) diff --git a/allianceauth/analytics/tests/test_utils.py b/allianceauth/analytics/tests/test_utils.py index b9a22329..60ee44bd 100644 --- a/allianceauth/analytics/tests/test_utils.py +++ b/allianceauth/analytics/tests/test_utils.py @@ -1,7 +1,6 @@ from django.apps import apps from allianceauth.authentication.models import User -from esi.models import Token -from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons +from allianceauth.analytics.utils import install_stat_users, install_stat_addons from django.test.testcases import TestCase diff --git a/allianceauth/authentication/decorators.py b/allianceauth/authentication/decorators.py index d9d93faa..13b4ed0a 100644 --- a/allianceauth/authentication/decorators.py +++ b/allianceauth/authentication/decorators.py @@ -1,6 +1,3 @@ -from django.urls import include -from django.contrib.auth.decorators import user_passes_test -from django.core.exceptions import PermissionDenied from functools import wraps from typing import Callable, Iterable, Optional diff --git a/allianceauth/authentication/models.py b/allianceauth/authentication/models.py index 714f1934..3df43b4a 100644 --- a/allianceauth/authentication/models.py +++ b/allianceauth/authentication/models.py @@ -5,7 +5,6 @@ from django.db import models, transaction from django.utils.translation import gettext_lazy as _ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo from allianceauth.notifications import notify -from django.conf import settings from .managers import CharacterOwnershipManager, StateManager diff --git a/allianceauth/authentication/tests/__init__.py b/allianceauth/authentication/tests/__init__.py index 13410df4..474a2c20 100644 --- a/allianceauth/authentication/tests/__init__.py +++ b/allianceauth/authentication/tests/__init__.py @@ -1,9 +1,3 @@ -from django.db.models.signals import ( - m2m_changed, - post_save, - pre_delete, - pre_save -) from django.urls import reverse from unittest import mock diff --git a/allianceauth/authentication/tests/test_middleware.py b/allianceauth/authentication/tests/test_middleware.py index 70335e58..10bd7ed0 100644 --- a/allianceauth/authentication/tests/test_middleware.py +++ b/allianceauth/authentication/tests/test_middleware.py @@ -1,4 +1,3 @@ -from unittest import mock from allianceauth.authentication.middleware import UserSettingsMiddleware from unittest.mock import Mock from django.http import HttpResponse diff --git a/allianceauth/authentication/tests/test_signals.py b/allianceauth/authentication/tests/test_signals.py index 8f7ba716..1e54e785 100644 --- a/allianceauth/authentication/tests/test_signals.py +++ b/allianceauth/authentication/tests/test_signals.py @@ -4,16 +4,10 @@ from allianceauth.eveonline.models import ( EveCorporationInfo, EveAllianceInfo ) -from django.db.models.signals import ( - pre_save, - post_save, - pre_delete, - m2m_changed -) +from django.db.models.signals import post_save from allianceauth.tests.auth_utils import AuthUtils from django.test.testcases import TestCase -from unittest.mock import Mock from . import patch diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 2e8f4e2c..ea73a01f 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -11,7 +11,6 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required, user_passes_test -from django.contrib.auth.models import User from django.core import signing from django.http import JsonResponse from django.shortcuts import redirect, render diff --git a/allianceauth/crontab/tests/test_utils.py b/allianceauth/crontab/tests/test_utils.py index 48592c99..563d42cf 100644 --- a/allianceauth/crontab/tests/test_utils.py +++ b/allianceauth/crontab/tests/test_utils.py @@ -1,5 +1,3 @@ -# myapp/tests/test_tasks.py - import logging from unittest.mock import patch from django.test import TestCase diff --git a/allianceauth/custom_css/widgets.py b/allianceauth/custom_css/widgets.py index 32ed07dd..f3da39cb 100644 --- a/allianceauth/custom_css/widgets.py +++ b/allianceauth/custom_css/widgets.py @@ -5,9 +5,6 @@ Form widgets for custom_css app # Django from django import forms -# Alliance Auth -from allianceauth.custom_css.models import CustomCSS - class CssEditorWidget(forms.Textarea): """ diff --git a/allianceauth/eveonline/autogroups/tests/test_models.py b/allianceauth/eveonline/autogroups/tests/test_models.py index f9c59389..4b04471d 100644 --- a/allianceauth/eveonline/autogroups/tests/test_models.py +++ b/allianceauth/eveonline/autogroups/tests/test_models.py @@ -1,6 +1,5 @@ from django.test import TestCase from django.contrib.auth.models import Group -from django.db import transaction from allianceauth.tests.auth_utils import AuthUtils diff --git a/allianceauth/eveonline/evelinks/tests/test_evelinks.py b/allianceauth/eveonline/evelinks/tests/test_evelinks.py index 09aa30fb..1b520edc 100644 --- a/allianceauth/eveonline/evelinks/tests/test_evelinks.py +++ b/allianceauth/eveonline/evelinks/tests/test_evelinks.py @@ -1,8 +1,6 @@ from django.test import TestCase -from ...models import EveCharacter, EveCorporationInfo, EveAllianceInfo from .. import dotlan, zkillboard, evewho, eveimageserver -from ...templatetags import evelinks class TestEveWho(TestCase): diff --git a/allianceauth/fleetactivitytracking/views.py b/allianceauth/fleetactivitytracking/views.py index 232fa456..fa7cd204 100644 --- a/allianceauth/fleetactivitytracking/views.py +++ b/allianceauth/fleetactivitytracking/views.py @@ -8,7 +8,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User from django.core.exceptions import ValidationError, ObjectDoesNotExist -from django.shortcuts import render, redirect, get_object_or_404, Http404 +from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ from esi.decorators import token_required diff --git a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py index 3e3e829a..66117b4f 100644 --- a/allianceauth/menu/tests/templatetags/test_menu_menu_items.py +++ b/allianceauth/menu/tests/templatetags/test_menu_menu_items.py @@ -186,7 +186,7 @@ class TestRenderDefaultMenu(TestCase): classes = "fa-solid fa-users-gear" url_name = "groupmanagement:management" - def render(Self, request): + def render(self, request): # simulate no perms return "" diff --git a/allianceauth/notifications/tests/test_templatetags.py b/allianceauth/notifications/tests/test_templatetags.py index 587970be..626d7226 100644 --- a/allianceauth/notifications/tests/test_templatetags.py +++ b/allianceauth/notifications/tests/test_templatetags.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, Mock +from unittest.mock import patch from django.test import TestCase, override_settings diff --git a/allianceauth/notifications/tests/test_views.py b/allianceauth/notifications/tests/test_views.py index 7fa95bc8..2e8024da 100644 --- a/allianceauth/notifications/tests/test_views.py +++ b/allianceauth/notifications/tests/test_views.py @@ -1,6 +1,6 @@ import json -from unittest.mock import patch, Mock +from unittest.mock import patch from django.test import TestCase, RequestFactory from django.urls import reverse diff --git a/allianceauth/permissions_tool/views.py b/allianceauth/permissions_tool/views.py index e845a466..3552e04c 100644 --- a/allianceauth/permissions_tool/views.py +++ b/allianceauth/permissions_tool/views.py @@ -1,7 +1,7 @@ import logging from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import Permission, User +from django.contrib.auth.models import Permission from django.db.models import Count from django.shortcuts import render, Http404 diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 084d8e49..ebfaa04b 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -1,13 +1,6 @@ from string import Formatter -from django.urls import include, re_path from typing import Iterable, Optional -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist -from django.template.loader import render_to_string -from django.urls import include, re_path -from django.utils.functional import cached_property - from allianceauth.hooks import get_hooks from allianceauth.menu.hooks import MenuItemHook from django.conf import settings diff --git a/allianceauth/services/modules/discord/tests/piloting_tasks.py b/allianceauth/services/modules/discord/tests/piloting_tasks.py index 4b1d0c7b..715d1b80 100644 --- a/allianceauth/services/modules/discord/tests/piloting_tasks.py +++ b/allianceauth/services/modules/discord/tests/piloting_tasks.py @@ -37,7 +37,6 @@ import random from django.contrib.auth.models import User, Group -from allianceauth.services.modules.discord.models import DiscordUser from allianceauth.utils.cache import get_redis_client logger = logging.getLogger('allianceauth') diff --git a/allianceauth/services/modules/discourse/manager.py b/allianceauth/services/modules/discourse/manager.py index 03cbbdee..fde966ea 100644 --- a/allianceauth/services/modules/discourse/manager.py +++ b/allianceauth/services/modules/discourse/manager.py @@ -1,5 +1,4 @@ import logging -import requests import re from django.conf import settings from django.core.cache import cache diff --git a/allianceauth/services/modules/discourse/views.py b/allianceauth/services/modules/discourse/views.py index a777fbb7..8a19f8e7 100644 --- a/allianceauth/services/modules/discourse/views.py +++ b/allianceauth/services/modules/discourse/views.py @@ -1,7 +1,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ from .manager import DiscourseManager diff --git a/allianceauth/services/modules/ips4/tasks.py b/allianceauth/services/modules/ips4/tasks.py index b6c5dce8..e2ba70c0 100644 --- a/allianceauth/services/modules/ips4/tasks.py +++ b/allianceauth/services/modules/ips4/tasks.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from allianceauth.services.hooks import NameFormatter diff --git a/allianceauth/tests/test_auth_utils.py b/allianceauth/tests/test_auth_utils.py index 2fa5215c..027e6c1c 100644 --- a/allianceauth/tests/test_auth_utils.py +++ b/allianceauth/tests/test_auth_utils.py @@ -1,12 +1,6 @@ -from unittest import mock - from django.contrib.auth.models import User, Group, Permission from django.test import TestCase -from allianceauth.eveonline.models import ( - EveCorporationInfo, EveAllianceInfo, EveCharacter -) - from .auth_utils import AuthUtils diff --git a/allianceauth/urls.py b/allianceauth/urls.py index 71a43b26..16adfa9c 100644 --- a/allianceauth/urls.py +++ b/allianceauth/urls.py @@ -1,6 +1,5 @@ from typing import List, Iterable, Callable -from django.urls import include import esi.urls from django.conf import settings from django.contrib import admin From 28cb62f373c9c2b73af032c9960648e37db8cbcb Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 19 Feb 2025 09:31:49 +0100 Subject: [PATCH 077/178] [ADD] Check for `ESI_USER_CONTACT_EMAIL` --- allianceauth/checks.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index 7235ad8a..aae8b9c1 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -16,19 +16,40 @@ B = Configuration @register() def django_settings(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that Django settings are correctly configured + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + errors: List[CheckMessage] = [] + + # SITE_URL if hasattr(settings, "SITE_URL"): if settings.SITE_URL[-1] == "/": errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005")) else: errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006")) + # CSRF_TRUSTED_ORIGINS if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"): if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS: errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007")) else: errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008")) + # ESI_USER_CONTACT_EMAIL + if hasattr(settings, "ESI_USER_CONTACT_EMAIL"): + if settings.ESI_USER_CONTACT_EMAIL == "": + errors.append(Error("'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.", hint="", id="allianceauth.checks.B009")) + else: + errors.append(Error("No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.", hint="", id="allianceauth.checks.B010")) + return errors From 929485a8f97f28fb3e7c3432249ac1c07a6cf192 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 19 Feb 2025 09:39:22 +0100 Subject: [PATCH 078/178] [ADD] Check for empty `SITE_URL` --- allianceauth/checks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index aae8b9c1..cceabbfc 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -31,8 +31,10 @@ def django_settings(app_configs, **kwargs) -> List[CheckMessage]: # SITE_URL if hasattr(settings, "SITE_URL"): - if settings.SITE_URL[-1] == "/": - errors.append(Warning("'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005")) + if settings.SITE_URL == "": + errors.append(Error("'SITE_URL' is empty.", hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)", id="allianceauth.checks.B011")) + elif settings.SITE_URL[-1] == "/": + errors.append(Warning("'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005")) else: errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006")) From 0498f5bb1b4e8055ea7a3ffb45c439ad7716a25c Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 19 Feb 2025 10:00:18 +0100 Subject: [PATCH 079/178] [MISC] Format for better readability --- allianceauth/checks.py | 453 +++++++++++++++++++++++++++++++++++------ 1 file changed, 395 insertions(+), 58 deletions(-) diff --git a/allianceauth/checks.py b/allianceauth/checks.py index cceabbfc..7544bc11 100644 --- a/allianceauth/checks.py +++ b/allianceauth/checks.py @@ -1,3 +1,7 @@ +""" +Django system checks for Alliance Auth +""" + from typing import List from django import db from django.core.checks import CheckMessage, Error, register, Warning @@ -29,58 +33,159 @@ def django_settings(app_configs, **kwargs) -> List[CheckMessage]: errors: List[CheckMessage] = [] - # SITE_URL + # Check for SITE_URL if hasattr(settings, "SITE_URL"): + # Check if SITE_URL is empty if settings.SITE_URL == "": - errors.append(Error("'SITE_URL' is empty.", hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)", id="allianceauth.checks.B011")) + errors.append( + Error( + msg="'SITE_URL' is empty.", + hint="Make sure to set 'SITE_URL' to the URL of your Auth instance. (Without trailing slash)", + id="allianceauth.checks.B011", + ) + ) + # Check if SITE_URL has a trailing slash elif settings.SITE_URL[-1] == "/": - errors.append(Warning("'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.", hint="", id="allianceauth.checks.B005")) + errors.append( + Warning( + msg="'SITE_URL' has a trailing slash. This may lead to incorrect links being generated by Auth.", + hint="", + id="allianceauth.checks.B005", + ) + ) + # SITE_URL not found else: - errors.append(Error("No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", hint="", id="allianceauth.checks.B006")) + errors.append( + Error( + msg="No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules.", + hint="", + id="allianceauth.checks.B006", + ) + ) - # CSRF_TRUSTED_ORIGINS + # Check for CSRF_TRUSTED_ORIGINS if hasattr(settings, "CSRF_TRUSTED_ORIGINS") and hasattr(settings, "SITE_URL"): + # Check if SITE_URL is not in CSRF_TRUSTED_ORIGINS if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS: - errors.append(Warning("'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", hint="", id="allianceauth.checks.B007")) + errors.append( + Warning( + msg="'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified.", + hint="", + id="allianceauth.checks.B007", + ) + ) + # CSRF_TRUSTED_ORIGINS not found else: - errors.append(Error("No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", hint="", id="allianceauth.checks.B008")) + errors.append( + Error( + msg="No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified", + hint="", + id="allianceauth.checks.B008", + ) + ) - # ESI_USER_CONTACT_EMAIL + # Check for ESI_USER_CONTACT_EMAIL if hasattr(settings, "ESI_USER_CONTACT_EMAIL"): + # Check if ESI_USER_CONTACT_EMAIL is empty if settings.ESI_USER_CONTACT_EMAIL == "": - errors.append(Error("'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.", hint="", id="allianceauth.checks.B009")) + errors.append( + Error( + msg="'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.", + hint="", + id="allianceauth.checks.B009", + ) + ) + # ESI_USER_CONTACT_EMAIL not found else: - errors.append(Error("No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.", hint="", id="allianceauth.checks.B010")) + errors.append( + Error( + msg="No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.", + hint="", + id="allianceauth.checks.B010", + ) + ) return errors @register() def system_package_redis(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that Redis is a supported version + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + allianceauth_redis_install_link = "https://allianceauth.readthedocs.io/en/latest/installation/allianceauth.html#redis-and-other-tools" errors: List[CheckMessage] = [] try: - redis_version = Pep440Version(get_redis_client().info()['redis_version']) + redis_version = Pep440Version(get_redis_client().info()["redis_version"]) except InvalidVersion: errors.append(Warning("Unable to confirm Redis Version")) + return errors - if redis_version.major == 7 and redis_version.minor == 2 and timezone.now() > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc): - errors.append(Error(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint=allianceauth_redis_install_link, id="allianceauth.checks.A001")) + if ( + redis_version.major == 7 + and redis_version.minor == 2 + and timezone.now() + > timezone.datetime(year=2025, month=8, day=31, tzinfo=timezone.utc) + ): + errors.append( + Error( + msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested", + hint=allianceauth_redis_install_link, + id="allianceauth.checks.A001", + ) + ) elif redis_version.major == 7 and redis_version.minor == 0: - errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint=allianceauth_redis_install_link, id="allianceauth.checks.A002")) + errors.append( + Warning( + msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested", + hint=allianceauth_redis_install_link, + id="allianceauth.checks.A002", + ) + ) elif redis_version.major == 6 and redis_version.minor == 2: - errors.append(Warning(f"Redis {redis_version.public} in Security Support only, Updating Suggested", hint=allianceauth_redis_install_link, id="allianceauth.checks.A018")) + errors.append( + Warning( + msg=f"Redis {redis_version.public} in Security Support only, Updating Suggested", + hint=allianceauth_redis_install_link, + id="allianceauth.checks.A018", + ) + ) elif redis_version.major in [6, 5]: - errors.append(Error(f"Redis {redis_version.public} EOL", hint=allianceauth_redis_install_link, id="allianceauth.checks.A003")) + errors.append( + Error( + msg=f"Redis {redis_version.public} EOL", + hint=allianceauth_redis_install_link, + id="allianceauth.checks.A003", + ) + ) return errors @register() def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that MySQL is a supported version + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + mysql_quick_guide_link = "https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/" errors: List[CheckMessage] = [] @@ -88,108 +193,302 @@ def system_package_mysql(app_configs, **kwargs) -> List[CheckMessage]: for connection in db.connections.all(): if connection.vendor == "mysql": try: - mysql_version = Pep440Version(".".join(str(i) for i in connection.mysql_version)) + mysql_version = Pep440Version( + ".".join(str(i) for i in connection.mysql_version) + ) except InvalidVersion: errors.append(Warning("Unable to confirm MySQL Version")) + return errors # MySQL 8 if mysql_version.major == 8: - if mysql_version.minor == 4 and timezone.now() > timezone.datetime(year=2032, month=4, day=30, tzinfo=timezone.utc): - errors.append(Error(f"MySQL {mysql_version.public} EOL", hint=mysql_quick_guide_link, id="allianceauth.checks.A004")) + if mysql_version.minor == 4 and timezone.now() > timezone.datetime( + year=2032, month=4, day=30, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MySQL {mysql_version.public} EOL", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A004", + ) + ) elif mysql_version.minor == 3: - errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint=mysql_quick_guide_link, id="allianceauth.checks.A005")) + errors.append( + Warning( + msg=f"MySQL {mysql_version.public} Non LTS", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A005", + ) + ) elif mysql_version.minor == 2: - errors.append(Warning(f"MySQL {mysql_version.public} Non LTS", hint=mysql_quick_guide_link, id="allianceauth.checks.A006")) + errors.append( + Warning( + msg=f"MySQL {mysql_version.public} Non LTS", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A006", + ) + ) elif mysql_version.minor == 1: - errors.append(Error(f"MySQL {mysql_version.public} EOL", hint=mysql_quick_guide_link, id="allianceauth.checks.A007")) - elif mysql_version.minor == 0 and timezone.now() > timezone.datetime(year=2026, month=4, day=30, tzinfo=timezone.utc): - errors.append(Error(f"MySQL {mysql_version.public} EOL", hint=mysql_quick_guide_link, id="allianceauth.checks.A008")) + errors.append( + Error( + msg=f"MySQL {mysql_version.public} EOL", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A007", + ) + ) + elif mysql_version.minor == 0 and timezone.now() > timezone.datetime( + year=2026, month=4, day=30, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MySQL {mysql_version.public} EOL", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A008", + ) + ) # MySQL below 8 # This will also catch Mariadb 5.x elif mysql_version.major < 8: - errors.append(Error(f"MySQL or MariaDB {mysql_version.public} EOL", hint=mysql_quick_guide_link, id="allianceauth.checks.A009")) + errors.append( + Error( + msg=f"MySQL or MariaDB {mysql_version.public} EOL", + hint=mysql_quick_guide_link, + id="allianceauth.checks.A009", + ) + ) + return errors @register() def system_package_mariadb(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that MariaDB is a supported version + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + mariadb_download_link = "https://mariadb.org/download/?t=repo-config" errors: List[CheckMessage] = [] for connection in db.connections.all(): - if connection.vendor == "mysql": # Still to find a way to determine MySQL vs MariaDB + # TODO: Find a way to determine MySQL vs. MariaDB + if connection.vendor == "mysql": try: - mariadb_version = Pep440Version(".".join(str(i) for i in connection.mysql_version)) + mariadb_version = Pep440Version( + ".".join(str(i) for i in connection.mysql_version) + ) except InvalidVersion: errors.append(Warning("Unable to confirm MariaDB Version")) + return errors # MariaDB 11 if mariadb_version.major == 11: - if mariadb_version.minor == 4 and timezone.now() > timezone.datetime(year=2029, month=5, day=19, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A010")) + if mariadb_version.minor == 4 and timezone.now() > timezone.datetime( + year=2029, month=5, day=19, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A010", + ) + ) elif mariadb_version.minor == 2: - errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint=mariadb_download_link, id="allianceauth.checks.A018")) + errors.append( + Warning( + msg=f"MariaDB {mariadb_version.public} Non LTS", + hint=mariadb_download_link, + id="allianceauth.checks.A018", + ) + ) - if timezone.now() > timezone.datetime(year=2024, month=11, day=21, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A011")) + if timezone.now() > timezone.datetime( + year=2024, month=11, day=21, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A011", + ) + ) elif mariadb_version.minor == 1: - errors.append(Warning(f"MariaDB {mariadb_version.public} Non LTS", hint=mariadb_download_link, id="allianceauth.checks.A019")) + errors.append( + Warning( + msg=f"MariaDB {mariadb_version.public} Non LTS", + hint=mariadb_download_link, + id="allianceauth.checks.A019", + ) + ) - if timezone.now() > timezone.datetime(year=2024, month=8, day=21, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A012")) - elif mariadb_version.minor in [0, 3]: # Demote versions down here once EOL - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A013")) + if timezone.now() > timezone.datetime( + year=2024, month=8, day=21, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A012", + ) + ) + # Demote versions down here once EOL + elif mariadb_version.minor in [0, 3]: + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A013", + ) + ) # MariaDB 10 elif mariadb_version.major == 10: - if mariadb_version.minor == 11 and timezone.now() > timezone.datetime(year=2028, month=2, day=10, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A014")) - elif mariadb_version.minor == 6 and timezone.now() > timezone.datetime(year=2026, month=7, day=6, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A0015")) - elif mariadb_version.minor == 5 and timezone.now() > timezone.datetime(year=2025, month=6, day=24, tzinfo=timezone.utc): - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A016")) - elif mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: # Demote versions down here once EOL - errors.append(Error(f"MariaDB {mariadb_version.public} EOL", hint=mariadb_download_link, id="allianceauth.checks.A017")) + if mariadb_version.minor == 11 and timezone.now() > timezone.datetime( + year=2028, month=2, day=10, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A014", + ) + ) + elif mariadb_version.minor == 6 and timezone.now() > timezone.datetime( + year=2026, month=7, day=6, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A0015", + ) + ) + elif mariadb_version.minor == 5 and timezone.now() > timezone.datetime( + year=2025, month=6, day=24, tzinfo=timezone.utc + ): + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A016", + ) + ) + # Demote versions down here once EOL + elif mariadb_version.minor in [0, 1, 2, 3, 4, 7, 9, 10]: + errors.append( + Error( + msg=f"MariaDB {mariadb_version.public} EOL", + hint=mariadb_download_link, + id="allianceauth.checks.A017", + ) + ) return errors @register() def system_package_sqlite(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that SQLite is a supported version + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + errors: List[CheckMessage] = [] + for connection in db.connections.all(): if connection.vendor == "sqlite": try: - sqlite_version = Pep440Version(".".join(str(i) for i in sqlite_version_info)) + sqlite_version = Pep440Version( + ".".join(str(i) for i in sqlite_version_info) + ) except InvalidVersion: errors.append(Warning("Unable to confirm SQLite Version")) + return errors if sqlite_version.major == 3 and sqlite_version.minor < 27: - errors.append(Error(f"SQLite {sqlite_version.public} Unsupported by Django", hint="https://pkgs.org/download/sqlite3", id="allianceauth.checks.A020")) + errors.append( + Error( + msg=f"SQLite {sqlite_version.public} Unsupported by Django", + hint="https://pkgs.org/download/sqlite3", + id="allianceauth.checks.A020", + ) + ) + return errors @register() def sql_settings(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that SQL settings are correctly configured + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + errors: List[CheckMessage] = [] + for connection in db.connections.all(): if connection.vendor == "mysql": try: if connection.settings_dict["OPTIONS"]["charset"] != "utf8mb4": - errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001")) + errors.append( + Error( + msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", + id="allianceauth.checks.B001", + ) + ) except KeyError: - errors.append(Error(f"SQL Charset is not set to utf8mb4 DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001")) + errors.append( + Error( + msg=f"SQL Charset is not set to utf8mb4 DB: {connection.alias}", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", + id="allianceauth.checks.B001", + ) + ) # This hasn't actually been set on AA yet # try: - # if connection.settings_dict["OPTIONS"]["collation"] != "utf8mb4_unicode_ci": - # errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001")) + # if ( + # connection.settings_dict["OPTIONS"]["collation"] + # != "utf8mb4_unicode_ci" + # ): + # errors.append( + # Error( + # msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", + # hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", + # id="allianceauth.checks.B001", + # ) + # ) # except KeyError: - # errors.append(Error(f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", id="allianceauth.checks.B001")) + # errors.append( + # Error( + # msg=f"SQL Collation is not set to utf8mb4_unicode_ci DB:{connection.alias}", + # hint="https://gitlab.com/allianceauth/allianceauth/-/commit/89be2456fb2d741b86417e889da9b6129525bec8", + # id="allianceauth.checks.B001", + # ) + # ) # if connection.vendor == "sqlite": @@ -198,19 +497,57 @@ def sql_settings(app_configs, **kwargs) -> List[CheckMessage]: @register() def celery_settings(app_configs, **kwargs) -> List[CheckMessage]: + """ + Check that Celery settings are correctly configured + + :param app_configs: + :type app_configs: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + errors: List[CheckMessage] = [] try: - if current_app.conf.broker_transport_options != {'priority_steps': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'queue_order_strategy': 'priority'}: - errors.append(Error("Celery Priorities are not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003")) + if current_app.conf.broker_transport_options != { + "priority_steps": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "queue_order_strategy": "priority", + }: + errors.append( + Error( + msg="Celery Priorities are not set correctly", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", + id="allianceauth.checks.B003", + ) + ) except KeyError: - errors.append(Error("Celery Priorities are not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", id="allianceauth.checks.B003")) + errors.append( + Error( + msg="Celery Priorities are not set", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/8861ec0a61790eca0261f1adc1cc04ca5f243cbc", + id="allianceauth.checks.B003", + ) + ) try: - if current_app.conf.broker_connection_retry_on_startup != True: - errors.append(Error("Celery broker_connection_retry_on_startup not set correctly", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004")) + if not current_app.conf.broker_connection_retry_on_startup: + errors.append( + Error( + msg="Celery broker_connection_retry_on_startup not set correctly", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", + id="allianceauth.checks.B004", + ) + ) except KeyError: - errors.append(Error("Celery broker_connection_retry_on_startup not set", hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", id="allianceauth.checks.B004")) + errors.append( + Error( + msg="Celery broker_connection_retry_on_startup not set", + hint="https://gitlab.com/allianceauth/allianceauth/-/commit/380c41400b535447839e5552df2410af35a75280", + id="allianceauth.checks.B004", + ) + ) return errors From 2de57b334baf74df9cf4e42f40009b7004b48c36 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 19 Feb 2025 10:09:00 +0100 Subject: [PATCH 080/178] [ADD] `ESI_USER_CONTACT_EMAIL` to test settings --- tests/settings_all.py | 1 + tests/settings_core.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/settings_all.py b/tests/settings_all.py index 5bddee70..6063b972 100644 --- a/tests/settings_all.py +++ b/tests/settings_all.py @@ -47,6 +47,7 @@ CACHES = { ESI_SSO_CLIENT_ID = "dummy" ESI_SSO_CLIENT_SECRET = "dummy" ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" +ESI_USER_CONTACT_EMAIL = "mail@dummy.net" ######################## # XenForo Configuration diff --git a/tests/settings_core.py b/tests/settings_core.py index 6403cfa8..b95218ed 100644 --- a/tests/settings_core.py +++ b/tests/settings_core.py @@ -37,3 +37,4 @@ ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED = True # disable for tests ESI_SSO_CLIENT_ID = "dummy" ESI_SSO_CLIENT_SECRET = "dummy" ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" +ESI_USER_CONTACT_EMAIL = "mail@dummy.net" From 3acb6516503a882ad6b48345e3544cb960600e18 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Wed, 19 Feb 2025 10:18:35 +0100 Subject: [PATCH 081/178] [CHANGE] Update test --- allianceauth/eveonline/tests/test_providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/eveonline/tests/test_providers.py b/allianceauth/eveonline/tests/test_providers.py index 95f4f069..ea8df739 100644 --- a/allianceauth/eveonline/tests/test_providers.py +++ b/allianceauth/eveonline/tests/test_providers.py @@ -723,5 +723,5 @@ class TestEveSwaggerProvider(TestCase): my_client = my_provider.client operation = my_client.Universe.get_universe_factions() self.assertEqual( - operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0' + operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 mail@dummy.net' ) From c1e2449084537fffd76d8e66fd65431ec47190c4 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Tue, 25 Feb 2025 09:16:59 +0000 Subject: [PATCH 082/178] Crontab fixes --- allianceauth/crontab/schedulers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/allianceauth/crontab/schedulers.py b/allianceauth/crontab/schedulers.py index 0b36eead..9a678cf5 100644 --- a/allianceauth/crontab/schedulers.py +++ b/allianceauth/crontab/schedulers.py @@ -20,6 +20,10 @@ class OffsetDatabaseScheduler(DatabaseScheduler): Takes the Celery Schedule from local.py and applies our AA Framework Cron Offset, if apply_offset is true Otherwise it passes it through as normal """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + def update_from_dict(self, mapping): s = {} @@ -36,10 +40,10 @@ class OffsetDatabaseScheduler(DatabaseScheduler): for name, entry_fields in mapping.items(): try: - apply_offset = entry_fields.pop("apply_offset", False) + apply_offset = entry_fields.pop("apply_offset", False) # Ensure this pops before django tries to save to ORM entry = self.Entry.from_entry(name, app=self.app, **entry_fields) - if entry.model.enabled and apply_offset: + if apply_offset: schedule_obj = entry.schedule if isinstance(schedule_obj, schedules.crontab): offset_cs = CrontabSchedule.from_schedule(offset_cron(schedule_obj)) @@ -55,7 +59,8 @@ class OffsetDatabaseScheduler(DatabaseScheduler): entry.model.save() logger.debug(f"Offset applied for '{name}' due to 'apply_offset' = True.") - s[name] = entry + if entry.model.enabled: + s[name] = entry except Exception as e: logger.exception("Error updating schedule for %s: %r", name, e) From 960c9625fe0037affb9dbb06e7e2221b5dd6c864 Mon Sep 17 00:00:00 2001 From: Joel Falknau Date: Tue, 25 Feb 2025 19:29:36 +1000 Subject: [PATCH 083/178] Version Bump 4.6.2a --- allianceauth/__init__.py | 2 +- allianceauth/locale/en/LC_MESSAGES/django.po | 474 +++++++++---------- docker/.env.example | 2 +- docker/Dockerfile | 2 +- 4 files changed, 237 insertions(+), 243 deletions(-) diff --git a/allianceauth/__init__.py b/allianceauth/__init__.py index a748cc09..564ea211 100644 --- a/allianceauth/__init__.py +++ b/allianceauth/__init__.py @@ -5,7 +5,7 @@ manage online service access. # This will make sure the app is always imported when # Django starts so that shared_task will use this app. -__version__ = '4.6.1' +__version__ = '4.6.2a' __title__ = 'Alliance Auth' __url__ = 'https://gitlab.com/allianceauth/allianceauth' NAME = f'{__title__} v{__version__}' diff --git a/allianceauth/locale/en/LC_MESSAGES/django.po b/allianceauth/locale/en/LC_MESSAGES/django.po index 0a06e636..1ab09cd4 100644 --- a/allianceauth/locale/en/LC_MESSAGES/django.po +++ b/allianceauth/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-21 12:59+1000\n" +"POT-Creation-Date: 2025-02-25 19:17+1000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: allianceauth/analytics/models.py:26 +#: allianceauth/analytics/models.py:22 msgid "Google Analytics Universal" msgstr "" -#: allianceauth/analytics/models.py:27 +#: allianceauth/analytics/models.py:23 msgid "Google Analytics V4" msgstr "" @@ -35,7 +35,7 @@ msgid "" "esi on the EVE Online Discord. https://www.eveonline.com/discord" msgstr "" -#: allianceauth/authentication/decorators.py:52 +#: allianceauth/authentication/decorators.py:49 msgid "A main character is required to perform that action. Add one below." msgstr "" @@ -48,91 +48,91 @@ msgstr "" msgid "You are not allowed to add or remove these restricted groups: %s" msgstr "" -#: allianceauth/authentication/models.py:71 -#: allianceauth/project_template/project_name/settings/base.py:99 +#: allianceauth/authentication/models.py:70 +#: allianceauth/project_template/project_name/settings/base.py:105 msgid "English" msgstr "" -#: allianceauth/authentication/models.py:72 +#: allianceauth/authentication/models.py:71 msgid "Czech" msgstr "" -#: allianceauth/authentication/models.py:73 -#: allianceauth/project_template/project_name/settings/base.py:101 +#: allianceauth/authentication/models.py:72 +#: allianceauth/project_template/project_name/settings/base.py:107 msgid "German" msgstr "" -#: allianceauth/authentication/models.py:74 -#: allianceauth/project_template/project_name/settings/base.py:102 +#: allianceauth/authentication/models.py:73 +#: allianceauth/project_template/project_name/settings/base.py:108 msgid "Spanish" msgstr "" -#: allianceauth/authentication/models.py:75 -#: allianceauth/project_template/project_name/settings/base.py:103 +#: allianceauth/authentication/models.py:74 +#: allianceauth/project_template/project_name/settings/base.py:109 msgid "Italian" msgstr "" -#: allianceauth/authentication/models.py:76 -#: allianceauth/project_template/project_name/settings/base.py:104 +#: allianceauth/authentication/models.py:75 +#: allianceauth/project_template/project_name/settings/base.py:110 msgid "Japanese" msgstr "" -#: allianceauth/authentication/models.py:77 -#: allianceauth/project_template/project_name/settings/base.py:105 +#: allianceauth/authentication/models.py:76 +#: allianceauth/project_template/project_name/settings/base.py:111 msgid "Korean" msgstr "" -#: allianceauth/authentication/models.py:78 -#: allianceauth/project_template/project_name/settings/base.py:106 +#: allianceauth/authentication/models.py:77 +#: allianceauth/project_template/project_name/settings/base.py:112 msgid "French" msgstr "" -#: allianceauth/authentication/models.py:79 -#: allianceauth/project_template/project_name/settings/base.py:109 +#: allianceauth/authentication/models.py:78 +#: allianceauth/project_template/project_name/settings/base.py:115 msgid "Russian" msgstr "" -#: allianceauth/authentication/models.py:80 -#: allianceauth/project_template/project_name/settings/base.py:107 +#: allianceauth/authentication/models.py:79 +#: allianceauth/project_template/project_name/settings/base.py:113 msgid "Dutch" msgstr "" -#: allianceauth/authentication/models.py:81 -#: allianceauth/project_template/project_name/settings/base.py:108 +#: allianceauth/authentication/models.py:80 +#: allianceauth/project_template/project_name/settings/base.py:114 msgid "Polish" msgstr "" -#: allianceauth/authentication/models.py:82 -#: allianceauth/project_template/project_name/settings/base.py:110 +#: allianceauth/authentication/models.py:81 +#: allianceauth/project_template/project_name/settings/base.py:116 msgid "Ukrainian" msgstr "" -#: allianceauth/authentication/models.py:83 -#: allianceauth/project_template/project_name/settings/base.py:111 +#: allianceauth/authentication/models.py:82 +#: allianceauth/project_template/project_name/settings/base.py:117 msgid "Simplified Chinese" msgstr "" -#: allianceauth/authentication/models.py:99 +#: allianceauth/authentication/models.py:98 #: allianceauth/menu/templates/menu/menu-user.html:42 msgid "Language" msgstr "" -#: allianceauth/authentication/models.py:104 +#: allianceauth/authentication/models.py:103 #: allianceauth/templates/allianceauth/night-toggle.html:6 msgid "Night Mode" msgstr "" -#: allianceauth/authentication/models.py:108 +#: allianceauth/authentication/models.py:107 #: allianceauth/menu/templates/menu/menu-user.html:46 msgid "Theme" msgstr "" -#: allianceauth/authentication/models.py:125 +#: allianceauth/authentication/models.py:124 #, python-format msgid "State changed to: %s" msgstr "" -#: allianceauth/authentication/models.py:126 +#: allianceauth/authentication/models.py:125 #, python-format msgid "Your user's state is now: %(state)s" msgstr "" @@ -167,9 +167,9 @@ msgstr "" #: allianceauth/authentication/templates/authentication/dashboard_characters.html:22 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:89 #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:31 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:32 #: allianceauth/hrapplications/templates/hrapplications/view.html:61 -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:32 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:33 msgid "Name" msgstr "" @@ -179,7 +179,7 @@ msgid "Corp" msgstr "" #: allianceauth/authentication/templates/authentication/dashboard_characters.html:24 -#: allianceauth/corputils/templates/corputils/corpstats.html:125 +#: allianceauth/corputils/templates/corputils/corpstats.html:126 #: allianceauth/hrapplications/templates/hrapplications/view.html:63 msgid "Alliance" msgstr "" @@ -192,43 +192,43 @@ msgstr "" msgid "State:" msgstr "" -#: allianceauth/authentication/templates/authentication/tokens.html:6 -#: allianceauth/authentication/templates/authentication/tokens.html:10 +#: allianceauth/authentication/templates/authentication/tokens.html:7 +#: allianceauth/authentication/templates/authentication/tokens.html:11 #: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62 msgid "Token Management" msgstr "" -#: allianceauth/authentication/templates/authentication/tokens.html:16 +#: allianceauth/authentication/templates/authentication/tokens.html:17 msgid "" "This page is a best attempt, but backups or database logs can still contain " -"your tokens. Always revoke tokens on https://community.eveonline.com/support/" -"third-party-applications/ where possible." -msgstr "" - -#: allianceauth/authentication/templates/authentication/tokens.html:22 -msgid "Scopes" +"your tokens. Always revoke tokens on https://developers.eveonline.com/" +"authorized-apps where possible." msgstr "" #: allianceauth/authentication/templates/authentication/tokens.html:23 +msgid "Scopes" +msgstr "" + +#: allianceauth/authentication/templates/authentication/tokens.html:24 #: allianceauth/hrapplications/templates/hrapplications/management.html:37 #: allianceauth/hrapplications/templates/hrapplications/management.html:124 #: allianceauth/hrapplications/templates/hrapplications/management.html:168 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:35 #: allianceauth/hrapplications/templates/hrapplications/view.html:94 -#: allianceauth/srp/templates/srp/data.html:82 +#: allianceauth/srp/templates/srp/data.html:83 #: allianceauth/srp/templates/srp/management.html:53 msgid "Actions" msgstr "" -#: allianceauth/authentication/templates/authentication/tokens.html:24 -#: allianceauth/corputils/templates/corputils/corpstats.html:123 -#: allianceauth/corputils/templates/corputils/corpstats.html:163 -#: allianceauth/corputils/templates/corputils/corpstats.html:210 -#: allianceauth/corputils/templates/corputils/search.html:16 +#: allianceauth/authentication/templates/authentication/tokens.html:25 +#: allianceauth/corputils/templates/corputils/corpstats.html:124 +#: allianceauth/corputils/templates/corputils/corpstats.html:164 +#: allianceauth/corputils/templates/corputils/corpstats.html:211 +#: allianceauth/corputils/templates/corputils/search.html:17 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:36 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:41 -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:29 -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:28 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:30 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:55 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:118 msgid "Character" @@ -267,49 +267,49 @@ msgstr "" msgid "Invalid or expired activation link." msgstr "" -#: allianceauth/authentication/views.py:158 +#: allianceauth/authentication/views.py:157 #, python-format msgid "" "Cannot change main character to %(char)s: character owned by a different " "account." msgstr "" -#: allianceauth/authentication/views.py:165 +#: allianceauth/authentication/views.py:164 #, python-format msgid "Changed main character to %s" msgstr "" -#: allianceauth/authentication/views.py:179 +#: allianceauth/authentication/views.py:178 #, python-format msgid "Added %(name)s to your account." msgstr "" -#: allianceauth/authentication/views.py:181 +#: allianceauth/authentication/views.py:180 #, python-format msgid "Failed to add %(name)s to your account: they already have an account." msgstr "" -#: allianceauth/authentication/views.py:226 +#: allianceauth/authentication/views.py:225 msgid "" "Unable to authenticate as the selected character. Please log in with the " "main character associated with this account." msgstr "" -#: allianceauth/authentication/views.py:293 +#: allianceauth/authentication/views.py:292 msgid "Registration token has expired." msgstr "" -#: allianceauth/authentication/views.py:354 +#: allianceauth/authentication/views.py:353 msgid "" "Sent confirmation email. Please follow the link to confirm your email " "address." msgstr "" -#: allianceauth/authentication/views.py:360 +#: allianceauth/authentication/views.py:359 msgid "Confirmed your email address. Please login to continue." msgstr "" -#: allianceauth/authentication/views.py:366 +#: allianceauth/authentication/views.py:365 msgid "Registration of new accounts is not allowed at this time." msgstr "" @@ -334,39 +334,39 @@ msgstr "" msgid "Search all corporations..." msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:45 +#: allianceauth/corputils/templates/corputils/corpstats.html:46 msgid "Mains" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:59 +#: allianceauth/corputils/templates/corputils/corpstats.html:60 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:34 msgid "Members" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:73 +#: allianceauth/corputils/templates/corputils/corpstats.html:74 msgid "Unregistered" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:79 +#: allianceauth/corputils/templates/corputils/corpstats.html:80 msgid "Last update:" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:85 +#: allianceauth/corputils/templates/corputils/corpstats.html:86 msgid "Update Now" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:100 +#: allianceauth/corputils/templates/corputils/corpstats.html:101 msgid "Main character" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:101 +#: allianceauth/corputils/templates/corputils/corpstats.html:102 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:29 msgid "Registered characters" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:124 -#: allianceauth/corputils/templates/corputils/search.html:17 -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:30 +#: allianceauth/corputils/templates/corputils/corpstats.html:125 +#: allianceauth/corputils/templates/corputils/search.html:18 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31 #: allianceauth/hrapplications/templates/hrapplications/management.html:35 #: allianceauth/hrapplications/templates/hrapplications/management.html:122 #: allianceauth/hrapplications/templates/hrapplications/management.html:166 @@ -375,16 +375,16 @@ msgstr "" msgid "Corporation" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:141 -#: allianceauth/corputils/templates/corputils/corpstats.html:177 -#: allianceauth/corputils/templates/corputils/corpstats.html:190 -#: allianceauth/corputils/templates/corputils/corpstats.html:222 -#: allianceauth/corputils/templates/corputils/search.html:30 +#: allianceauth/corputils/templates/corputils/corpstats.html:142 +#: allianceauth/corputils/templates/corputils/corpstats.html:178 +#: allianceauth/corputils/templates/corputils/corpstats.html:191 +#: allianceauth/corputils/templates/corputils/corpstats.html:223 +#: allianceauth/corputils/templates/corputils/search.html:31 msgid "Killboard" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:165 -#: allianceauth/corputils/templates/corputils/search.html:19 +#: allianceauth/corputils/templates/corputils/corpstats.html:166 +#: allianceauth/corputils/templates/corputils/search.html:20 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:32 #: allianceauth/hrapplications/templates/hrapplications/management.html:121 #: allianceauth/hrapplications/templates/hrapplications/management.html:165 @@ -393,21 +393,21 @@ msgstr "" msgid "Main Character" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:166 -#: allianceauth/corputils/templates/corputils/search.html:20 +#: allianceauth/corputils/templates/corputils/corpstats.html:167 +#: allianceauth/corputils/templates/corputils/search.html:21 msgid "Main Corporation" msgstr "" -#: allianceauth/corputils/templates/corputils/corpstats.html:167 -#: allianceauth/corputils/templates/corputils/search.html:21 +#: allianceauth/corputils/templates/corputils/corpstats.html:168 +#: allianceauth/corputils/templates/corputils/search.html:22 msgid "Main Alliance" msgstr "" -#: allianceauth/corputils/templates/corputils/search.html:8 +#: allianceauth/corputils/templates/corputils/search.html:9 msgid "Search Results" msgstr "" -#: allianceauth/corputils/templates/corputils/search.html:18 +#: allianceauth/corputils/templates/corputils/search.html:19 msgid "zKillboard" msgstr "" @@ -419,6 +419,26 @@ msgstr "" msgid "Failed to gather corporation statistics with selected token." msgstr "" +#: allianceauth/crontab/models.py:13 +msgid "Minute Offset" +msgstr "" + +#: allianceauth/crontab/models.py:14 +msgid "Hour Offset" +msgstr "" + +#: allianceauth/crontab/models.py:15 +msgid "Day of Month Offset" +msgstr "" + +#: allianceauth/crontab/models.py:16 +msgid "Month of Year Offset" +msgstr "" + +#: allianceauth/crontab/models.py:17 +msgid "Day of Week Offset" +msgstr "" + #: allianceauth/custom_css/apps.py:13 allianceauth/custom_css/models.py:36 #: allianceauth/custom_css/models.py:37 allianceauth/custom_css/models.py:47 msgid "Custom CSS" @@ -517,9 +537,9 @@ msgid "Delete fat" msgstr "" #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:35 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:35 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:36 #: allianceauth/hrapplications/templates/hrapplications/view.html:41 -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:30 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:31 msgid "User" msgstr "" @@ -527,7 +547,7 @@ msgstr "" #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:42 #: allianceauth/optimer/form.py:13 allianceauth/timerboard/form.py:37 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:17 -#: allianceauth/timerboard/templates/timerboard/timertable.html:9 +#: allianceauth/timerboard/templates/timerboard/timertable.html:10 msgid "System" msgstr "" @@ -542,7 +562,7 @@ msgstr "" #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:44 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:92 #: allianceauth/templates/allianceauth/top-menu.html:23 -#: allianceauth/timerboard/templates/timerboard/timertable.html:11 +#: allianceauth/timerboard/templates/timerboard/timertable.html:12 msgid "Eve Time" msgstr "" @@ -601,7 +621,7 @@ msgstr "" #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:74 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:90 -#: allianceauth/timerboard/templates/timerboard/timertable.html:13 +#: allianceauth/timerboard/templates/timerboard/timertable.html:14 msgid "Creator" msgstr "" @@ -709,17 +729,17 @@ msgid "" msgstr "" #: allianceauth/groupmanagement/auth_hooks.py:18 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:17 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:18 msgid "Group Management" msgstr "" #: allianceauth/groupmanagement/auth_hooks.py:51 -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:34 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35 msgid "Groups" msgstr "" #: allianceauth/groupmanagement/forms.py:18 -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:33 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:34 msgid "Users" msgstr "" @@ -822,84 +842,84 @@ msgstr "" msgid "Date when this entry was created" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:8 -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:12 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:9 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:13 msgid "Audit Log" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:17 -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:18 -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:21 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:18 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:19 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22 #: allianceauth/timerboard/templates/timerboard/index_button.html:4 msgid "Back" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:27 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:28 msgid "Date/Time" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:28 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:29 msgid "Requestor" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:32 #: allianceauth/optimer/templates/optimer/dashboard.ops.html:15 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:16 msgid "Type" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:32 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 #: allianceauth/notifications/templates/notifications/list_partial.html:8 #: allianceauth/optimer/templates/optimer/fleetoptable.html:20 -#: allianceauth/timerboard/templates/timerboard/timertable.html:16 +#: allianceauth/timerboard/templates/timerboard/timertable.html:17 msgid "Action" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:33 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:34 msgid "Actor" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:47 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:48 msgid "Removed" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:59 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:60 msgid "All times displayed are EVE/UTC." msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:66 +#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:67 msgid "No entries found for this group." msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:9 -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:13 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:10 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:14 msgid "Group Members" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:30 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:56 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:119 -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:32 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:33 msgid "Organization" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:49 -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:75 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:50 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:76 msgid "Group leader" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:60 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:61 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:85 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:148 #: allianceauth/permissions_tool/templates/permissions_tool/audit_row.html:18 msgid "(unknown)" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:65 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:66 msgid "Remove from group" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:80 +#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:81 msgid "No group members to list." msgstr "" @@ -913,7 +933,7 @@ msgid "Join/Leave Requests" msgstr "" #: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:24 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:32 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:33 #: allianceauth/optimer/templates/optimer/fleetoptable.html:10 msgid "Description" msgstr "" @@ -923,7 +943,7 @@ msgstr "" #: allianceauth/hrapplications/templates/hrapplications/management.html:123 #: allianceauth/hrapplications/templates/hrapplications/management.html:167 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:34 -#: allianceauth/srp/templates/srp/data.html:80 +#: allianceauth/srp/templates/srp/data.html:81 msgid "Status" msgstr "" @@ -960,49 +980,49 @@ msgstr "" msgid "No groups to list." msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:7 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:11 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:8 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:12 msgid "Available Groups" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:34 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:35 msgid "Leaders" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:36 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:37 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:57 #: allianceauth/groupmanagement/templates/groupmanagement/index.html:120 -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:29 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:30 #: allianceauth/services/modules/openfire/forms.py:6 msgid "Group" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:69 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:70 msgid "Leave" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:73 -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:88 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:74 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:89 #: allianceauth/hrapplications/templates/hrapplications/management.html:46 #: allianceauth/hrapplications/templates/hrapplications/management.html:95 #: allianceauth/hrapplications/templates/hrapplications/management.html:138 #: allianceauth/hrapplications/templates/hrapplications/management.html:182 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:25 -#: allianceauth/srp/templates/srp/data.html:116 +#: allianceauth/srp/templates/srp/data.html:117 #: allianceauth/srp/templates/srp/management.html:87 msgid "Pending" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:79 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:80 msgid "Join" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:83 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:84 msgid "Request" msgstr "" -#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:98 +#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:99 msgid "No groups available." msgstr "" @@ -1202,7 +1222,7 @@ msgstr "" #: allianceauth/hrapplications/templates/hrapplications/management.html:185 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:48 #: allianceauth/hrapplications/templates/hrapplications/view.html:21 -#: allianceauth/srp/templates/srp/data.html:108 +#: allianceauth/srp/templates/srp/data.html:109 msgid "Approved" msgstr "" @@ -1210,7 +1230,7 @@ msgstr "" #: allianceauth/hrapplications/templates/hrapplications/management.html:143 #: allianceauth/hrapplications/templates/hrapplications/management.html:187 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:50 -#: allianceauth/srp/templates/srp/data.html:112 +#: allianceauth/srp/templates/srp/data.html:113 msgid "Rejected" msgstr "" @@ -1525,7 +1545,7 @@ msgid "Fleet Commander" msgstr "" #: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14 -#: allianceauth/srp/templates/srp/data.html:71 +#: allianceauth/srp/templates/srp/data.html:72 msgid "Additional Info" msgstr "" @@ -1576,7 +1596,7 @@ msgid "EVE Time" msgstr "" #: allianceauth/optimer/templates/optimer/fleetoptable.html:14 -#: allianceauth/timerboard/templates/timerboard/timertable.html:12 +#: allianceauth/timerboard/templates/timerboard/timertable.html:13 msgid "Local Time" msgstr "" @@ -1589,7 +1609,7 @@ msgid "Fleet Operation Management" msgstr "" #: allianceauth/optimer/templates/optimer/management.html:28 -#: allianceauth/timerboard/templates/timerboard/view.html:31 +#: allianceauth/timerboard/templates/timerboard/view.html:32 msgid "Current Eve Time:" msgstr "" @@ -1598,7 +1618,7 @@ msgid "Next Fleet Operations" msgstr "" #: allianceauth/optimer/templates/optimer/management.html:44 -#: allianceauth/timerboard/templates/timerboard/view.html:62 +#: allianceauth/timerboard/templates/timerboard/view.html:63 msgid "No upcoming timers." msgstr "" @@ -1607,7 +1627,7 @@ msgid "Past Fleet Operations" msgstr "" #: allianceauth/optimer/templates/optimer/management.html:60 -#: allianceauth/timerboard/templates/timerboard/view.html:80 +#: allianceauth/timerboard/templates/timerboard/view.html:81 msgid "No past timers." msgstr "" @@ -1635,50 +1655,50 @@ msgstr "" msgid "Saved changes to operation timer for %(opname)s." msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:6 -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:10 -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:16 -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:10 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:7 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:11 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:17 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:11 msgid "Permissions Audit" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:31 +#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:32 msgid "User / Character" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:6 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:7 msgid "Permissions Overview" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:17 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:18 msgid "Showing only applied permissions" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:18 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:19 msgid "Show All" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:20 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:21 msgid "Showing all permissions" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:21 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:22 msgid "Show Applied" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:29 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:30 msgid "App" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:30 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:31 msgid "Model" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:31 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:32 msgid "Code Name" msgstr "" -#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35 +#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:36 msgid "States" msgstr "" @@ -1856,45 +1876,45 @@ msgstr "" msgid "Deactivated IPSuite4 account." msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:6 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:7 msgid "Mumble" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:10 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:11 msgid "Mumble History" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:22 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:23 msgid "Server Connection History" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:31 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:32 msgid "Displayed Name" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:32 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:33 msgid "Release" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:33 -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:67 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:34 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:68 msgid "Version" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:34 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:35 msgid "Last Connect" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:35 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:36 msgid "Last Disconnect" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:47 -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:59 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:48 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:60 msgid "Server Connection Breakdown" msgstr "" -#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:68 +#: allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html:69 msgid "Number" msgstr "" @@ -2200,7 +2220,7 @@ msgid "SRP Fleet Create" msgstr "" #: allianceauth/srp/templates/srp/add.html:11 -#: allianceauth/srp/templates/srp/data.html:11 +#: allianceauth/srp/templates/srp/data.html:12 #: allianceauth/srp/templates/srp/management.html:11 #: allianceauth/srp/templates/srp/request.html:11 #: allianceauth/srp/templates/srp/update.html:11 @@ -2223,73 +2243,73 @@ msgstr "" msgid "Give this link to the line members." msgstr "" -#: allianceauth/srp/templates/srp/data.html:7 -#: allianceauth/srp/templates/srp/data.html:38 +#: allianceauth/srp/templates/srp/data.html:8 +#: allianceauth/srp/templates/srp/data.html:39 msgid "SRP Fleet Data" msgstr "" -#: allianceauth/srp/templates/srp/data.html:16 +#: allianceauth/srp/templates/srp/data.html:17 msgid "View Fleets" msgstr "" -#: allianceauth/srp/templates/srp/data.html:25 +#: allianceauth/srp/templates/srp/data.html:26 msgid "Mark Incomplete" msgstr "" -#: allianceauth/srp/templates/srp/data.html:29 +#: allianceauth/srp/templates/srp/data.html:30 msgid "Mark Completed" msgstr "" -#: allianceauth/srp/templates/srp/data.html:47 -#: allianceauth/srp/templates/srp/data.html:138 -msgid "Total Losses:" -msgstr "" - #: allianceauth/srp/templates/srp/data.html:48 #: allianceauth/srp/templates/srp/data.html:139 +msgid "Total Losses:" +msgstr "" + +#: allianceauth/srp/templates/srp/data.html:49 +#: allianceauth/srp/templates/srp/data.html:140 #: allianceauth/srp/templates/srp/management.html:36 msgid "Total ISK Cost:" msgstr "" -#: allianceauth/srp/templates/srp/data.html:59 -#: allianceauth/srp/templates/srp/data.html:150 +#: allianceauth/srp/templates/srp/data.html:60 +#: allianceauth/srp/templates/srp/data.html:151 msgid "Are you sure you want to delete SRP requests?" msgstr "" -#: allianceauth/srp/templates/srp/data.html:69 +#: allianceauth/srp/templates/srp/data.html:70 msgid "Pilot Name" msgstr "" -#: allianceauth/srp/templates/srp/data.html:70 +#: allianceauth/srp/templates/srp/data.html:71 msgid "Killboard Link" msgstr "" -#: allianceauth/srp/templates/srp/data.html:72 +#: allianceauth/srp/templates/srp/data.html:73 msgid "Ship Type" msgstr "" -#: allianceauth/srp/templates/srp/data.html:73 +#: allianceauth/srp/templates/srp/data.html:74 msgid "Killboard Loss Amt" msgstr "" -#: allianceauth/srp/templates/srp/data.html:75 +#: allianceauth/srp/templates/srp/data.html:76 msgid "SRP ISK Cost" msgstr "" -#: allianceauth/srp/templates/srp/data.html:76 +#: allianceauth/srp/templates/srp/data.html:77 msgid "Click value to edit Enter to save & next ESC to cancel" msgstr "" -#: allianceauth/srp/templates/srp/data.html:79 +#: allianceauth/srp/templates/srp/data.html:80 msgid "Post Time" msgstr "" -#: allianceauth/srp/templates/srp/data.html:98 +#: allianceauth/srp/templates/srp/data.html:99 #: allianceauth/srp/templates/srp/management.html:70 msgid "Link" msgstr "" -#: allianceauth/srp/templates/srp/data.html:159 +#: allianceauth/srp/templates/srp/data.html:160 msgid "No SRP requests for this fleet." msgstr "" @@ -2562,7 +2582,7 @@ msgstr "" #: allianceauth/timerboard/form.py:36 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:14 -#: allianceauth/timerboard/templates/timerboard/timertable.html:7 +#: allianceauth/timerboard/templates/timerboard/timertable.html:8 msgid "Details" msgstr "" @@ -2579,7 +2599,7 @@ msgid "Timer Type" msgstr "" #: allianceauth/timerboard/form.py:41 -#: allianceauth/timerboard/templates/timerboard/timertable.html:8 +#: allianceauth/timerboard/templates/timerboard/timertable.html:9 msgid "Objective" msgstr "" @@ -2613,159 +2633,141 @@ msgstr "" #: allianceauth/timerboard/models.py:15 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:39 -#: allianceauth/timerboard/templates/timerboard/timertable.html:36 msgid "Friendly" msgstr "" #: allianceauth/timerboard/models.py:16 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:34 -#: allianceauth/timerboard/templates/timerboard/timertable.html:34 msgid "Hostile" msgstr "" #: allianceauth/timerboard/models.py:17 #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:44 -#: allianceauth/timerboard/templates/timerboard/timertable.html:38 msgid "Neutral" msgstr "" #: allianceauth/timerboard/models.py:24 -#: allianceauth/timerboard/templates/timerboard/timertable.html:48 msgid "POCO" msgstr "" #: allianceauth/timerboard/models.py:25 -#: allianceauth/timerboard/templates/timerboard/timertable.html:50 msgid "Orbital Skyhook" msgstr "" #: allianceauth/timerboard/models.py:26 -#: allianceauth/timerboard/templates/timerboard/timertable.html:52 -msgid "I-HUB" +msgid "Sovereignty Hub" msgstr "" #: allianceauth/timerboard/models.py:27 -#: allianceauth/timerboard/templates/timerboard/timertable.html:55 msgid "TCU" msgstr "" #: allianceauth/timerboard/models.py:28 -#: allianceauth/timerboard/templates/timerboard/timertable.html:57 msgid "POS [S]" msgstr "" #: allianceauth/timerboard/models.py:29 -#: allianceauth/timerboard/templates/timerboard/timertable.html:59 msgid "POS [M]" msgstr "" #: allianceauth/timerboard/models.py:30 -#: allianceauth/timerboard/templates/timerboard/timertable.html:61 msgid "POS [L]" msgstr "" #: allianceauth/timerboard/models.py:31 -#: allianceauth/timerboard/templates/timerboard/timertable.html:63 msgid "Astrahus" msgstr "" #: allianceauth/timerboard/models.py:32 -#: allianceauth/timerboard/templates/timerboard/timertable.html:65 msgid "Fortizar" msgstr "" #: allianceauth/timerboard/models.py:33 -#: allianceauth/timerboard/templates/timerboard/timertable.html:67 msgid "Keepstar" msgstr "" #: allianceauth/timerboard/models.py:34 -#: allianceauth/timerboard/templates/timerboard/timertable.html:69 msgid "Raitaru" msgstr "" #: allianceauth/timerboard/models.py:35 -#: allianceauth/timerboard/templates/timerboard/timertable.html:71 msgid "Azbel" msgstr "" #: allianceauth/timerboard/models.py:36 -#: allianceauth/timerboard/templates/timerboard/timertable.html:73 msgid "Sotiyo" msgstr "" #: allianceauth/timerboard/models.py:37 -#: allianceauth/timerboard/templates/timerboard/timertable.html:75 msgid "Athanor" msgstr "" #: allianceauth/timerboard/models.py:38 -#: allianceauth/timerboard/templates/timerboard/timertable.html:77 msgid "Tatara" msgstr "" #: allianceauth/timerboard/models.py:39 -msgid "Pharolux Cyno Beacon" +msgid "Cyno Beacon" msgstr "" #: allianceauth/timerboard/models.py:40 -msgid "Tenebrex Cyno Jammer" +msgid "Cyno Jammer" msgstr "" #: allianceauth/timerboard/models.py:41 -#: allianceauth/timerboard/templates/timerboard/timertable.html:83 msgid "Ansiblex Jump Gate" msgstr "" #: allianceauth/timerboard/models.py:42 -#: allianceauth/timerboard/templates/timerboard/timertable.html:85 -msgid "Moon Mining Cycle" +msgid "Mercenary Den" msgstr "" #: allianceauth/timerboard/models.py:43 -#: allianceauth/timerboard/templates/timerboard/timertable.html:87 -msgid "Metenox Moon Drill" +msgid "Moon Mining Cycle" msgstr "" #: allianceauth/timerboard/models.py:44 -#: allianceauth/timerboard/templates/timerboard/timertable.html:89 +msgid "Metenox Moon Drill" +msgstr "" + +#: allianceauth/timerboard/models.py:45 msgid "Other" msgstr "" -#: allianceauth/timerboard/models.py:51 +#: allianceauth/timerboard/models.py:52 msgid "Not Specified" msgstr "" -#: allianceauth/timerboard/models.py:52 +#: allianceauth/timerboard/models.py:53 msgid "Shield" msgstr "" -#: allianceauth/timerboard/models.py:53 +#: allianceauth/timerboard/models.py:54 msgid "Armor" msgstr "" -#: allianceauth/timerboard/models.py:54 +#: allianceauth/timerboard/models.py:55 msgid "Hull" msgstr "" -#: allianceauth/timerboard/models.py:55 +#: allianceauth/timerboard/models.py:56 msgid "Final" msgstr "" -#: allianceauth/timerboard/models.py:56 +#: allianceauth/timerboard/models.py:57 msgid "Anchoring" msgstr "" -#: allianceauth/timerboard/models.py:57 +#: allianceauth/timerboard/models.py:58 msgid "Unanchoring" msgstr "" -#: allianceauth/timerboard/models.py:58 +#: allianceauth/timerboard/models.py:59 msgid "Abandoned" msgstr "" #: allianceauth/timerboard/templates/timerboard/dashboard.timers.html:7 -#: allianceauth/timerboard/templates/timerboard/view.html:53 +#: allianceauth/timerboard/templates/timerboard/view.html:54 msgid "Upcoming Timers" msgstr "" @@ -2775,7 +2777,7 @@ msgstr "" #: allianceauth/timerboard/templates/timerboard/form.html:10 #: allianceauth/timerboard/templates/timerboard/timer_confirm_delete.html:10 -#: allianceauth/timerboard/templates/timerboard/view.html:13 +#: allianceauth/timerboard/templates/timerboard/view.html:14 msgid "Structure Timers" msgstr "" @@ -2799,7 +2801,7 @@ msgid "Create Timer" msgstr "" #: allianceauth/timerboard/templates/timerboard/timer_create_form.html:9 -#: allianceauth/timerboard/templates/timerboard/view.html:21 +#: allianceauth/timerboard/templates/timerboard/view.html:22 msgid "Create Structure Timer" msgstr "" @@ -2809,36 +2811,28 @@ msgstr "" msgid "Update Structure Timer" msgstr "" -#: allianceauth/timerboard/templates/timerboard/timertable.html:10 +#: allianceauth/timerboard/templates/timerboard/timertable.html:11 msgid "Structure" msgstr "" -#: allianceauth/timerboard/templates/timerboard/timertable.html:79 -msgid "Cyno Beacon" -msgstr "" - -#: allianceauth/timerboard/templates/timerboard/timertable.html:81 -msgid "Cyno Jammer" -msgstr "" - -#: allianceauth/timerboard/templates/timerboard/view.html:9 +#: allianceauth/timerboard/templates/timerboard/view.html:10 msgid "Structure Timer Management" msgstr "" -#: allianceauth/timerboard/templates/timerboard/view.html:40 +#: allianceauth/timerboard/templates/timerboard/view.html:41 msgid "Corporation Timers" msgstr "" -#: allianceauth/timerboard/templates/timerboard/view.html:71 +#: allianceauth/timerboard/templates/timerboard/view.html:72 msgid "Past Timers" msgstr "" -#: allianceauth/timerboard/views.py:85 +#: allianceauth/timerboard/views.py:175 #, python-format msgid "Added new timer in %(system)s at %(time)s." msgstr "" -#: allianceauth/timerboard/views.py:95 +#: allianceauth/timerboard/views.py:186 msgid "Saved changes to the timer." msgstr "" diff --git a/docker/.env.example b/docker/.env.example index 76958b0d..a6c71f7d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,7 +1,7 @@ PROTOCOL=https:// AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN% DOMAIN=%DOMAIN% -AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.6.1 +AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.6.2a # Nginx Proxy Manager PROXY_HTTP_PORT=80 diff --git a/docker/Dockerfile b/docker/Dockerfile index 28990230..113c8b93 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.11-slim -ARG AUTH_VERSION=v4.6.1 +ARG AUTH_VERSION=v4.6.2a ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION} ENV AUTH_USER=allianceauth ENV AUTH_GROUP=allianceauth From 4ccfe20c14c86cdf2ece3f6561b0196b7cb148b4 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Tue, 25 Feb 2025 11:52:25 +0100 Subject: [PATCH 084/178] [CHANGE] Set email to `dummy@example.net` --- allianceauth/eveonline/tests/test_providers.py | 2 +- tests/settings_all.py | 2 +- tests/settings_core.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/allianceauth/eveonline/tests/test_providers.py b/allianceauth/eveonline/tests/test_providers.py index ea8df739..e90b727b 100644 --- a/allianceauth/eveonline/tests/test_providers.py +++ b/allianceauth/eveonline/tests/test_providers.py @@ -723,5 +723,5 @@ class TestEveSwaggerProvider(TestCase): my_client = my_provider.client operation = my_client.Universe.get_universe_factions() self.assertEqual( - operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 mail@dummy.net' + operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 dummy@example.net' ) diff --git a/tests/settings_all.py b/tests/settings_all.py index 6063b972..309f9260 100644 --- a/tests/settings_all.py +++ b/tests/settings_all.py @@ -47,7 +47,7 @@ CACHES = { ESI_SSO_CLIENT_ID = "dummy" ESI_SSO_CLIENT_SECRET = "dummy" ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" -ESI_USER_CONTACT_EMAIL = "mail@dummy.net" +ESI_USER_CONTACT_EMAIL = "dummy@example.net" ######################## # XenForo Configuration diff --git a/tests/settings_core.py b/tests/settings_core.py index b95218ed..3222f5df 100644 --- a/tests/settings_core.py +++ b/tests/settings_core.py @@ -37,4 +37,4 @@ ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED = True # disable for tests ESI_SSO_CLIENT_ID = "dummy" ESI_SSO_CLIENT_SECRET = "dummy" ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" -ESI_USER_CONTACT_EMAIL = "mail@dummy.net" +ESI_USER_CONTACT_EMAIL = "dummy@example.net" From faa529a55b37dec4a55716953093ac67b2cac6ea Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Tue, 25 Feb 2025 17:33:02 +0100 Subject: [PATCH 085/178] [REMOVE] Stray `a` tag from `header-nav-brand` --- .../templates/services/mumble/mumble_connection_history.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html index 024133fe..d3e4bc11 100644 --- a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html +++ b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html @@ -8,7 +8,7 @@ {% endblock page_title %} {% block header_nav_brand %} - {% trans "Mumble History" %} - {{ mumble_url }} + {% trans "Mumble History" %} - {{ mumble_url }} {% endblock header_nav_brand %} {% block header_nav_collapse_left %} From 5559ce5fbb7a0545aa68451132413dcdf21d9af2 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Tue, 25 Feb 2025 18:01:58 +0100 Subject: [PATCH 086/178] [CHANGE] Return empty field when no datetime available --- .../services/mumble/mumble_connection_history.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html index 024133fe..8ba78bbe 100644 --- a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html +++ b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html @@ -105,6 +105,10 @@ { data: 'last_connect', render: (data) => { + if (data === null) { + return ''; + } + return moment(data).utc().format(MUMBLESTATS_DATETIME_FORMAT); }, className: 'text-end', @@ -112,6 +116,10 @@ { data: 'last_disconnect', render: (data) => { + if (data === null) { + return ''; + } + return moment(data).utc().format(MUMBLESTATS_DATETIME_FORMAT); }, className: 'text-end', From 8e9a53c4944977dc6cf9ebc8c2a89f0cc8ddcfd8 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Tue, 25 Feb 2025 18:03:17 +0100 Subject: [PATCH 087/178] [CHANGE] Localze datetime output --- .../services/mumble/mumble_connection_history.html | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html index 8ba78bbe..3f166fd3 100644 --- a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html +++ b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html @@ -84,10 +84,11 @@ {% include "bundles/moment-js.html" with locale=True %} {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %} + {% get_momentjs_language_code LANGUAGE_CODE as MOMENTJS_LCODE %} {% include 'bundles/jquery-js.html' %} + {% theme_js %} {% if user.is_authenticated %} @@ -148,6 +154,7 @@ {% block extra_javascript %} {% endblock extra_javascript %} +