diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 631650d6..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-[settings]
-profile=django
-sections=FUTURE,STDLIB,THIRDPARTY,DJANGO,ESI,FIRSTPARTY,LOCALFOLDER
-known_esi=esi
-known_django=django
-skip_gitignore=true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5e9e3fde..f7b92bac 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,12 +7,33 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- - id: check-case-conflict
- - id: check-json
- - id: check-xml
+ # Identify invalid files
+ - id: check-ast
- id: check-yaml
+ - id: check-json
+ - id: check-toml
+ - id: check-xml
+
+ # git checks
+ - id: check-merge-conflict
+ - id: check-added-large-files
+ args: [ --maxkb=1000 ]
+ - id: detect-private-key
+ - id: check-case-conflict
+
+ # Python checks
+# - id: check-docstring-first
+ - id: debug-statements
+# - id: requirements-txt-fixer
+ - id: fix-encoding-pragma
+ args: [ --remove ]
- id: fix-byte-order-marker
+
+ # General quality checks
+ - id: mixed-line-ending
+ args: [ --fix=lf ]
- id: trailing-whitespace
+ args: [ --markdown-linebreak-ext=md ]
exclude: |
(?x)(
\.min\.css|
@@ -21,6 +42,7 @@ repos:
\.mo|
swagger\.json
)
+ - id: check-executables-have-shebangs
- id: end-of-file-fixer
exclude: |
(?x)(
@@ -30,13 +52,9 @@ repos:
\.mo|
swagger\.json
)
- - id: mixed-line-ending
- args: [ '--fix=lf' ]
- - id: fix-encoding-pragma
- args: [ '--remove' ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
- rev: 2.7.1
+ rev: 2.7.2
hooks:
- id: editorconfig-checker
exclude: |
@@ -49,13 +67,13 @@ repos:
)
- repo: https://github.com/asottile/pyupgrade
- rev: v3.4.0
+ rev: v3.10.1
hooks:
- id: pyupgrade
args: [ --py38-plus ]
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.13.0
+ rev: 1.14.0
hooks:
- id: django-upgrade
args: [--target-version=4.2]
diff --git a/.tx/config_20230406134150.bak b/.tx/config_20230406134150.bak
old mode 100755
new mode 100644
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index d46a6471..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,7 +0,0 @@
-include LICENSE
-include README.md
-include MANIFEST.in
-graft allianceauth
-
-global-exclude __pycache__
-global-exclude *.py[co]
diff --git a/README.md b/README.md
old mode 100755
new mode 100644
diff --git a/allianceauth/__init__.py b/allianceauth/__init__.py
index 417f9297..c846863b 100644
--- a/allianceauth/__init__.py
+++ b/allianceauth/__init__.py
@@ -1,7 +1,11 @@
+"""An auth system for EVE Online to help in-game organizations
+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__ = '3.4.0'
+__version__ = '3.6.1'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'
diff --git a/allianceauth/authentication/decorators.py b/allianceauth/authentication/decorators.py
index 33bbf2e3..d9d93faa 100644
--- a/allianceauth/authentication/decorators.py
+++ b/allianceauth/authentication/decorators.py
@@ -2,17 +2,30 @@ 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 django.shortcuts import redirect
+from typing import Callable, Iterable, Optional
+
+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.utils.translation import gettext_lazy as _
-from django.contrib.auth.decorators import login_required
def user_has_main_character(user):
return bool(user.profile.main_character)
-def decorate_url_patterns(urls, decorator):
+def decorate_url_patterns(
+ urls, decorator: Callable, excluded_views: Optional[Iterable] = None
+):
+ """Decorate views given in url patterns except when they are explicitly excluded.
+
+ Args:
+ - urls: Django URL patterns
+ - decorator: Decorator to be added to each view
+ - exclude_views: Optional iterable of view names to be excluded
+ """
url_list, app_name, namespace = include(urls)
def process_patterns(url_patterns):
@@ -22,6 +35,8 @@ def decorate_url_patterns(urls, decorator):
process_patterns(pattern.url_patterns)
else:
# this is a pattern
+ if excluded_views and pattern.lookup_str in excluded_views:
+ return
pattern.callback = decorator(pattern.callback)
process_patterns(url_list)
diff --git a/allianceauth/authentication/hmac_urls.py b/allianceauth/authentication/hmac_urls.py
index 23c48ac1..61bcd99b 100644
--- a/allianceauth/authentication/hmac_urls.py
+++ b/allianceauth/authentication/hmac_urls.py
@@ -1,7 +1,5 @@
from allianceauth.authentication import views
-from django.urls import include
-from django.urls import re_path
-from django.urls import path
+from django.urls import include, re_path, path
urlpatterns = [
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
diff --git a/allianceauth/authentication/managers.py b/allianceauth/authentication/managers.py
old mode 100755
new mode 100644
diff --git a/allianceauth/authentication/migrations/0021_alter_userprofile_language.py b/allianceauth/authentication/migrations/0021_alter_userprofile_language.py
new file mode 100644
index 00000000..be9ab240
--- /dev/null
+++ b/allianceauth/authentication/migrations/0021_alter_userprofile_language.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.0.10 on 2023-05-28 15:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("authentication", "0020_userprofile_language_userprofile_night_mode"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="userprofile",
+ name="language",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("en", "English"),
+ ("de", "German"),
+ ("es", "Spanish"),
+ ("zh-hans", "Chinese Simplified"),
+ ("ru", "Russian"),
+ ("ko", "Korean"),
+ ("fr", "French"),
+ ("ja", "Japanese"),
+ ("it", "Italian"),
+ ("uk", "Ukrainian"),
+ ],
+ default="",
+ max_length=10,
+ verbose_name="Language",
+ ),
+ ),
+ ]
diff --git a/allianceauth/authentication/models.py b/allianceauth/authentication/models.py
old mode 100755
new mode 100644
index 80fdcd4a..6d7a06b8
--- a/allianceauth/authentication/models.py
+++ b/allianceauth/authentication/models.py
@@ -63,6 +63,22 @@ class UserProfile(models.Model):
class Meta:
default_permissions = ('change',)
+ class Language(models.TextChoices):
+ """
+ Choices for UserProfile.language
+ """
+
+ ENGLISH = 'en', _('English')
+ GERMAN = 'de', _('German')
+ SPANISH = 'es', _('Spanish')
+ CHINESE = 'zh-hans', _('Chinese Simplified')
+ RUSSIAN = 'ru', _('Russian')
+ KOREAN = 'ko', _('Korean')
+ FRENCH = 'fr', _('French')
+ JAPANESE = 'ja', _('Japanese')
+ ITALIAN = 'it', _('Italian')
+ UKRAINIAN = 'uk', _('Ukrainian')
+
user = models.OneToOneField(
User,
related_name='profile',
@@ -76,20 +92,9 @@ class UserProfile(models.Model):
State,
on_delete=models.SET_DEFAULT,
default=get_guest_state_pk)
- LANGUAGE_CHOICES = [
- ('en', _('English')),
- ('de', _('German')),
- ('es', _('Spanish')),
- ('zh-hans', _('Chinese Simplified')),
- ('ru', _('Russian')),
- ('ko', _('Korean')),
- ('fr', _('French')),
- ('ja', _('Japanese')),
- ('it', _('Italian')),
- ]
language = models.CharField(
_("Language"), max_length=10,
- choices=LANGUAGE_CHOICES,
+ choices=Language.choices,
blank=True,
default='')
night_mode = models.BooleanField(
diff --git a/allianceauth/authentication/task_statistics/counters.py b/allianceauth/authentication/task_statistics/counters.py
index bd9eaabd..06e2af83 100644
--- a/allianceauth/authentication/task_statistics/counters.py
+++ b/allianceauth/authentication/task_statistics/counters.py
@@ -1,35 +1,44 @@
-from collections import namedtuple
+"""Counters for Task Statistics."""
+
import datetime as dt
+from typing import NamedTuple, Optional
from .event_series import EventSeries
+from .helpers import ItemCounter
-
-"""Global series for counting task events."""
+# Global series for counting task events.
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS")
+running_tasks = ItemCounter("running_tasks")
-_TaskCounts = namedtuple(
- "_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
-)
+class _TaskCounts(NamedTuple):
+ succeeded: int
+ retried: int
+ failed: int
+ total: int
+ earliest_task: Optional[dt.datetime]
+ hours: int
+ running: int
def dashboard_results(hours: int) -> _TaskCounts:
- """Counts of all task events within the given timeframe."""
+ """Counts of all task events within the given time frame."""
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
- earliest_events = list()
+ earliest_events = []
succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
retried_count = retried_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
+ running_count = running_tasks.value()
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
@@ -37,4 +46,5 @@ def dashboard_results(hours: int) -> _TaskCounts:
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
+ running=running_count,
)
diff --git a/allianceauth/authentication/task_statistics/event_series.py b/allianceauth/authentication/task_statistics/event_series.py
index aead7c6a..9ac1f15d 100644
--- a/allianceauth/authentication/task_statistics/event_series.py
+++ b/allianceauth/authentication/task_statistics/event_series.py
@@ -1,61 +1,31 @@
+"""Event series for Task Statistics."""
+
import datetime as dt
import logging
from typing import List, Optional
from pytz import utc
-from redis import Redis, RedisError
+from redis import Redis
-from allianceauth.utils.cache import get_redis_client
+from .helpers import get_redis_client_or_stub
logger = logging.getLogger(__name__)
-class _RedisStub:
- """Stub of a Redis client.
-
- It's purpose is to prevent EventSeries objects from trying to access Redis
- when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
- """
-
- def delete(self, *args, **kwargs):
- pass
-
- def incr(self, *args, **kwargs):
- return 0
-
- def zadd(self, *args, **kwargs):
- pass
-
- def zcount(self, *args, **kwargs):
- pass
-
- def zrangebyscore(self, *args, **kwargs):
- pass
-
-
class EventSeries:
"""API for recording and analyzing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
- def __init__(self, key_id: str, redis: Redis = None) -> None:
- self._redis = get_redis_client() if not redis else redis
- try:
- if not self._redis.ping():
- raise RuntimeError()
- except (AttributeError, RedisError, RuntimeError):
- logger.exception(
- "Failed to establish a connection with Redis. "
- "This EventSeries object is disabled.",
- )
- self._redis = _RedisStub()
+ def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None:
+ self._redis = get_redis_client_or_stub() if not redis else redis
self._key_id = str(key_id)
self.clear()
@property
def is_disabled(self):
"""True when this object is disabled, e.g. Redis was not available at startup."""
- return isinstance(self._redis, _RedisStub)
+ return hasattr(self._redis, "IS_STUB")
@property
def _key_counter(self):
@@ -73,8 +43,8 @@ class EventSeries:
"""
if not event_time:
event_time = dt.datetime.utcnow()
- id = self._redis.incr(self._key_counter)
- self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
+ 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]:
"""List of all known events."""
@@ -95,15 +65,15 @@ class EventSeries:
self._redis.delete(self._key_counter)
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
- """Count of events, can be restricted to given timeframe.
+ """Count of events, can be restricted to given time frame.
Args:
- earliest: Date of first events to count(inclusive), or -infinite if not specified
- latest: Date of last events to count(inclusive), or +infinite if not specified
"""
- min = "-inf" if not earliest else earliest.timestamp()
- max = "+inf" if not latest else latest.timestamp()
- return self._redis.zcount(self._key_sorted_set, min=min, max=max)
+ minimum = "-inf" if not earliest else earliest.timestamp()
+ 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]:
"""Date/Time of first event. Returns `None` if series has no events.
@@ -111,10 +81,10 @@ class EventSeries:
Args:
- earliest: Date of first events to count(inclusive), or any if not specified
"""
- min = "-inf" if not earliest else earliest.timestamp()
+ minimum = "-inf" if not earliest else earliest.timestamp()
event = self._redis.zrangebyscore(
self._key_sorted_set,
- min,
+ minimum,
"+inf",
withscores=True,
start=0,
diff --git a/allianceauth/authentication/task_statistics/helpers.py b/allianceauth/authentication/task_statistics/helpers.py
new file mode 100644
index 00000000..b75fb39c
--- /dev/null
+++ b/allianceauth/authentication/task_statistics/helpers.py
@@ -0,0 +1,108 @@
+"""Helpers for Task Statistics."""
+
+import logging
+from typing import Optional
+
+from redis import Redis, RedisError
+
+from django.core.cache import cache
+
+from allianceauth.utils.cache import get_redis_client
+
+logger = logging.getLogger(__name__)
+
+
+class _RedisStub:
+ """Stub of a Redis client.
+
+ It's purpose is to prevent EventSeries objects from trying to access Redis
+ when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
+ """
+
+ IS_STUB = True
+
+ def delete(self, *args, **kwargs):
+ pass
+
+ def incr(self, *args, **kwargs):
+ return 0
+
+ def zadd(self, *args, **kwargs):
+ pass
+
+ def zcount(self, *args, **kwargs):
+ pass
+
+ def zrangebyscore(self, *args, **kwargs):
+ pass
+
+
+class ItemCounter:
+ """A process safe item counter.
+
+ Args:
+ - name: Unique name for the counter
+ - minimum: Counter can not go below the minimum, when set
+ - redis: A Redis client. Will use AA's cache client by default
+ """
+
+ CACHE_KEY_BASE = "allianceauth-item-counter"
+ DEFAULT_CACHE_TIMEOUT = 24 * 3600
+
+ def __init__(
+ self, name: str, minimum: Optional[int] = None, redis: Optional[Redis] = None
+ ) -> None:
+ if not name:
+ raise ValueError("Must define a name")
+
+ self._name = str(name)
+ self._minimum = minimum
+ self._redis = get_redis_client_or_stub() if not redis else redis
+
+ @property
+ def _cache_key(self) -> str:
+ return f"{self.CACHE_KEY_BASE}-{self._name}"
+
+ def reset(self, init_value: int = 0):
+ """Reset counter to initial value."""
+ with self._redis.lock(f"{self.CACHE_KEY_BASE}-reset"):
+ if self._minimum is not None and init_value < self._minimum:
+ raise ValueError("Can not reset below minimum")
+
+ cache.set(self._cache_key, init_value, self.DEFAULT_CACHE_TIMEOUT)
+
+ def incr(self, delta: int = 1):
+ """Increment counter by delta."""
+ try:
+ cache.incr(self._cache_key, delta)
+ except ValueError:
+ pass
+
+ def decr(self, delta: int = 1):
+ """Decrement counter by delta."""
+ with self._redis.lock(f"{self.CACHE_KEY_BASE}-decr"):
+ if self._minimum is not None and self.value() == self._minimum:
+ return
+ try:
+ cache.decr(self._cache_key, delta)
+ except ValueError:
+ pass
+
+ def value(self) -> Optional[int]:
+ """Return current value or None if not yet initialized."""
+ return cache.get(self._cache_key)
+
+
+def get_redis_client_or_stub() -> Redis:
+ """Return AA's default cache client or a stub if Redis is not available."""
+ redis = get_redis_client()
+ try:
+ if not redis.ping():
+ raise RuntimeError()
+ except (AttributeError, RedisError, RuntimeError):
+ logger.exception(
+ "Failed to establish a connection with Redis. "
+ "This EventSeries object is disabled.",
+ )
+ return _RedisStub()
+ return redis
diff --git a/allianceauth/authentication/task_statistics/signals.py b/allianceauth/authentication/task_statistics/signals.py
index 9c54520a..17665d65 100644
--- a/allianceauth/authentication/task_statistics/signals.py
+++ b/allianceauth/authentication/task_statistics/signals.py
@@ -1,14 +1,15 @@
+"""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_postrun, task_prerun, task_retry,
+ task_success, worker_ready,
)
from django.conf import settings
-from .counters import failed_tasks, retried_tasks, succeeded_tasks
+from .counters import (
+ failed_tasks, retried_tasks, running_tasks, succeeded_tasks,
+)
def reset_counters():
@@ -16,9 +17,11 @@ def reset_counters():
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
+ running_tasks.reset()
def is_enabled() -> bool:
+ """Return True if task statistics are enabled, else return False."""
return not bool(
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
)
@@ -52,3 +55,15 @@ def record_task_failed(*args, **kwargs):
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()
+
+
+@task_prerun.connect
+def record_task_prerun(*args, **kwargs):
+ if is_enabled():
+ running_tasks.incr()
+
+
+@task_postrun.connect
+def record_task_postrun(*args, **kwargs):
+ if is_enabled():
+ running_tasks.decr()
diff --git a/allianceauth/authentication/task_statistics/tests/test_counters.py b/allianceauth/authentication/task_statistics/tests/test_counters.py
index 2625d49d..284f86ca 100644
--- a/allianceauth/authentication/task_statistics/tests/test_counters.py
+++ b/allianceauth/authentication/task_statistics/tests/test_counters.py
@@ -8,25 +8,31 @@ from allianceauth.authentication.task_statistics.counters import (
succeeded_tasks,
retried_tasks,
failed_tasks,
+ running_tasks,
)
class TestDashboardResults(TestCase):
- def test_should_return_counts_for_given_timeframe_only(self):
+ def test_should_return_counts_for_given_time_frame_only(self):
# given
earliest_task = now() - dt.timedelta(minutes=15)
+
succeeded_tasks.clear()
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded_tasks.add(earliest_task)
succeeded_tasks.add()
succeeded_tasks.add()
+
retried_tasks.clear()
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
retried_tasks.add(now() - dt.timedelta(seconds=30))
retried_tasks.add()
+
failed_tasks.clear()
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add()
+
+ running_tasks.reset(8)
# when
results = dashboard_results(hours=1)
# then
@@ -35,12 +41,14 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
+ self.assertEqual(results.running, 8)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
+ running_tasks.reset()
# when
results = dashboard_results(hours=1)
# then
@@ -49,3 +57,4 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)
+ self.assertEqual(results.running, 0)
diff --git a/allianceauth/authentication/task_statistics/tests/test_event_series.py b/allianceauth/authentication/task_statistics/tests/test_event_series.py
index 70804cf4..17889180 100644
--- a/allianceauth/authentication/task_statistics/tests/test_event_series.py
+++ b/allianceauth/authentication/task_statistics/tests/test_event_series.py
@@ -1,48 +1,19 @@
import datetime as dt
-from unittest.mock import patch
from pytz import utc
-from redis import RedisError
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import (
EventSeries,
- _RedisStub,
)
+from allianceauth.authentication.task_statistics.helpers import _RedisStub
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase):
- def test_should_abort_without_redis_client(self):
- # when
- with patch(MODULE_PATH + ".get_redis_client") as mock:
- mock.return_value = None
- events = EventSeries("dummy")
- # then
- self.assertTrue(events._redis, _RedisStub)
- self.assertTrue(events.is_disabled)
-
- def test_should_disable_itself_if_redis_not_available_1(self):
- # when
- with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
- mock_get_master_client.return_value.ping.side_effect = RedisError
- events = EventSeries("dummy")
- # then
- self.assertIsInstance(events._redis, _RedisStub)
- self.assertTrue(events.is_disabled)
-
- def test_should_disable_itself_if_redis_not_available_2(self):
- # when
- with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
- mock_get_master_client.return_value.ping.return_value = False
- events = EventSeries("dummy")
- # then
- self.assertIsInstance(events._redis, _RedisStub)
- self.assertTrue(events.is_disabled)
-
def test_should_add_event(self):
# given
events = EventSeries("dummy")
@@ -166,3 +137,15 @@ class TestEventSeries(TestCase):
results = events.all()
# then
self.assertEqual(len(results), 2)
+
+ def test_should_not_report_as_disabled_when_initialized_normally(self):
+ # given
+ events = EventSeries("dummy")
+ # when/then
+ self.assertFalse(events.is_disabled)
+
+ def test_should_report_as_disabled_when_initialized_with_redis_stub(self):
+ # given
+ events = EventSeries("dummy", redis=_RedisStub())
+ # when/then
+ self.assertTrue(events.is_disabled)
diff --git a/allianceauth/authentication/task_statistics/tests/test_helpers.py b/allianceauth/authentication/task_statistics/tests/test_helpers.py
new file mode 100644
index 00000000..757ae38e
--- /dev/null
+++ b/allianceauth/authentication/task_statistics/tests/test_helpers.py
@@ -0,0 +1,142 @@
+from unittest import TestCase
+from unittest.mock import patch
+
+from redis import RedisError
+
+from allianceauth.authentication.task_statistics.helpers import (
+ ItemCounter, _RedisStub, get_redis_client_or_stub,
+)
+
+MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
+
+COUNTER_NAME = "test-counter"
+
+
+class TestItemCounter(TestCase):
+ def test_can_create_counter(self):
+ # when
+ counter = ItemCounter(COUNTER_NAME)
+ # then
+ self.assertIsInstance(counter, ItemCounter)
+
+ def test_can_reset_counter_to_default(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ # when
+ counter.reset()
+ # then
+ self.assertEqual(counter.value(), 0)
+
+ def test_can_reset_counter_to_custom_value(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ # when
+ counter.reset(42)
+ # then
+ self.assertEqual(counter.value(), 42)
+
+ def test_can_increment_counter_by_default(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(0)
+ # when
+ counter.incr()
+ # then
+ self.assertEqual(counter.value(), 1)
+
+ def test_can_increment_counter_by_custom_value(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(0)
+ # when
+ counter.incr(8)
+ # then
+ self.assertEqual(counter.value(), 8)
+
+ def test_can_decrement_counter_by_default(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(9)
+ # when
+ counter.decr()
+ # then
+ self.assertEqual(counter.value(), 8)
+
+ def test_can_decrement_counter_by_custom_value(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(9)
+ # when
+ counter.decr(8)
+ # then
+ self.assertEqual(counter.value(), 1)
+
+ def test_can_decrement_counter_below_zero(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(0)
+ # when
+ counter.decr(1)
+ # then
+ self.assertEqual(counter.value(), -1)
+
+ def test_can_not_decrement_counter_below_minimum(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME, minimum=0)
+ counter.reset(0)
+ # when
+ counter.decr(1)
+ # then
+ self.assertEqual(counter.value(), 0)
+
+ def test_can_not_reset_counter_below_minimum(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME, minimum=0)
+ # when/then
+ with self.assertRaises(ValueError):
+ counter.reset(-1)
+
+ def test_can_not_init_without_name(self):
+ # when/then
+ with self.assertRaises(ValueError):
+ ItemCounter(name="")
+
+ def test_can_ignore_invalid_values_when_incrementing(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(0)
+ # when
+ with patch(MODULE_PATH + ".cache.incr") as m:
+ m.side_effect = ValueError
+ counter.incr()
+ # then
+ self.assertEqual(counter.value(), 0)
+
+ def test_can_ignore_invalid_values_when_decrementing(self):
+ # given
+ counter = ItemCounter(COUNTER_NAME)
+ counter.reset(1)
+ # when
+ with patch(MODULE_PATH + ".cache.decr") as m:
+ m.side_effect = ValueError
+ counter.decr()
+ # then
+ self.assertEqual(counter.value(), 1)
+
+
+class TestGetRedisClient(TestCase):
+ def test_should_return_mock_if_redis_not_available_1(self):
+ # when
+ with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
+ mock_get_master_client.return_value.ping.side_effect = RedisError
+ result = get_redis_client_or_stub()
+ # then
+ self.assertIsInstance(result, _RedisStub)
+
+ def test_should_return_mock_if_redis_not_available_2(self):
+ # when
+ with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
+ mock_get_master_client.return_value.ping.return_value = False
+ result = get_redis_client_or_stub()
+ # then
+ self.assertIsInstance(result, _RedisStub)
diff --git a/allianceauth/authentication/task_statistics/tests/test_signals.py b/allianceauth/authentication/task_statistics/tests/test_signals.py
index fd1530fa..1e1634bd 100644
--- a/allianceauth/authentication/task_statistics/tests/test_signals.py
+++ b/allianceauth/authentication/task_statistics/tests/test_signals.py
@@ -22,11 +22,12 @@ from allianceauth.eveonline.tasks import update_character
class TestTaskSignals(TestCase):
fixtures = ["disable_analytics"]
- def test_should_record_successful_task(self):
- # given
+ def setUp(self) -> None:
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
+
+ def test_should_record_successful_task(self):
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -39,10 +40,6 @@ class TestTaskSignals(TestCase):
self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self):
- # given
- succeeded_tasks.clear()
- retried_tasks.clear()
- failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -55,10 +52,6 @@ class TestTaskSignals(TestCase):
self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self):
- # given
- succeeded_tasks.clear()
- retried_tasks.clear()
- failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
diff --git a/allianceauth/authentication/templates/public/login.html b/allianceauth/authentication/templates/public/login.html
index 11d4b792..11134f27 100644
--- a/allianceauth/authentication/templates/public/login.html
+++ b/allianceauth/authentication/templates/public/login.html
@@ -6,7 +6,7 @@
{% block page_title %}{% translate "Login" %}{% endblock %}
{% block middle_box_content %}
-
+
{% endblock %}
diff --git a/allianceauth/authentication/tests/test_decorators.py b/allianceauth/authentication/tests/test_decorators.py
index 69c3949e..5b729d01 100644
--- a/allianceauth/authentication/tests/test_decorators.py
+++ b/allianceauth/authentication/tests/test_decorators.py
@@ -4,16 +4,16 @@ from urllib import parse
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponse
-from django.shortcuts import reverse
from django.test import TestCase
from django.test.client import RequestFactory
+from django.urls import reverse, URLPattern
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
-from ..decorators import main_character_required
-from ..models import CharacterOwnership
+from ..decorators import decorate_url_patterns, main_character_required
+from ..models import CharacterOwnership
MODULE_PATH = 'allianceauth.authentication'
@@ -66,3 +66,33 @@ class DecoratorTestCase(TestCase):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)
+
+
+class TestDecorateUrlPatterns(TestCase):
+ def test_should_add_decorator_by_default(self):
+ # given
+ decorator = mock.MagicMock(name="decorator")
+ view = mock.MagicMock(name="view")
+ path = mock.MagicMock(spec=URLPattern, name="path")
+ path.callback = view
+ path.lookup_str = "my_lookup_str"
+ urls = [path]
+ urlconf_module = urls
+ # when
+ decorate_url_patterns(urlconf_module, decorator)
+ # then
+ self.assertEqual(path.callback, decorator(view))
+
+ def test_should_not_add_decorator_when_excluded(self):
+ # given
+ decorator = mock.MagicMock(name="decorator")
+ view = mock.MagicMock(name="view")
+ path = mock.MagicMock(spec=URLPattern, name="path")
+ path.callback = view
+ path.lookup_str = "my_lookup_str"
+ urls = [path]
+ urlconf_module = urls
+ # when
+ decorate_url_patterns(urlconf_module, decorator, excluded_views=["my_lookup_str"])
+ # then
+ self.assertEqual(path.callback, view)
diff --git a/allianceauth/eveonline/views.py b/allianceauth/eveonline/views.py
old mode 100755
new mode 100644
diff --git a/allianceauth/groupmanagement/views.py b/allianceauth/groupmanagement/views.py
old mode 100755
new mode 100644
diff --git a/allianceauth/hrapplications/admin.py b/allianceauth/hrapplications/admin.py
old mode 100755
new mode 100644
diff --git a/allianceauth/hrapplications/forms.py b/allianceauth/hrapplications/forms.py
old mode 100755
new mode 100644
diff --git a/allianceauth/hrapplications/models.py b/allianceauth/hrapplications/models.py
old mode 100755
new mode 100644
diff --git a/allianceauth/hrapplications/views.py b/allianceauth/hrapplications/views.py
old mode 100755
new mode 100644
index fe7c93f5..789428e2
--- a/allianceauth/hrapplications/views.py
+++ b/allianceauth/hrapplications/views.py
@@ -57,7 +57,7 @@ def hr_application_create_view(request, form_id=None):
app_form = get_object_or_404(ApplicationForm, id=form_id)
if request.method == "POST":
if Application.objects.filter(user=request.user).filter(form=app_form).exists():
- logger.warn(f"User {request.user} attempting to duplicate application to {app_form.corp}")
+ logger.warning(f"User {request.user} attempting to duplicate application to {app_form.corp}")
else:
application = Application(user=request.user, form=app_form)
application.save()
@@ -92,7 +92,7 @@ def hr_application_personal_view(request, app_id):
}
return render(request, 'hrapplications/view.html', context=context)
else:
- logger.warn(f"User {request.user} not authorized to view {app}")
+ logger.warning(f"User {request.user} not authorized to view {app}")
return redirect('hrapplications:personal_view')
@@ -105,9 +105,9 @@ def hr_application_personal_removal(request, app_id):
logger.info(f"User {request.user} deleting {app}")
app.delete()
else:
- logger.warn(f"User {request.user} attempting to delete reviewed app {app}")
+ logger.warning(f"User {request.user} attempting to delete reviewed app {app}")
else:
- logger.warn(f"User {request.user} not authorized to delete {app}")
+ logger.warning(f"User {request.user} not authorized to delete {app}")
return redirect('hrapplications:index')
@@ -132,7 +132,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.warn("User %s does not have permission to add ApplicationComments" % request.user)
+ logger.warning("User %s does not have permission to add ApplicationComments" % request.user)
return redirect('hrapplications:view', app_id)
else:
logger.debug("Returning blank HRApplication comment form.")
@@ -171,7 +171,7 @@ def hr_application_approve(request, app_id):
app.save()
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
else:
- logger.warn(f"User {request.user} not authorized to approve {app}")
+ logger.warning(f"User {request.user} not authorized to approve {app}")
return redirect('hrapplications:index')
@@ -187,7 +187,7 @@ def hr_application_reject(request, app_id):
app.save()
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
else:
- logger.warn(f"User {request.user} not authorized to reject {app}")
+ logger.warning(f"User {request.user} not authorized to reject {app}")
return redirect('hrapplications:index')
@@ -208,7 +208,7 @@ def hr_application_search(request):
app_list = app_list.filter(
form__corp__corporation_id=request.user.profile.main_character.corporation_id)
except AttributeError:
- logger.warn(
+ logger.warning(
"User %s missing main character model: unable to filter applications to search" % request.user)
applications = app_list.filter(
@@ -246,6 +246,6 @@ def hr_application_mark_in_progress(request, app_id):
app.save()
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
else:
- logger.warn(
+ logger.warning(
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
return redirect("hrapplications:view", app_id)
diff --git a/allianceauth/notifications/views.py b/allianceauth/notifications/views.py
index e23bf03f..40bc88f5 100644
--- a/allianceauth/notifications/views.py
+++ b/allianceauth/notifications/views.py
@@ -44,7 +44,7 @@ def notification_view(request, notif_id):
notif.mark_viewed()
return render(request, 'notifications/view.html', context)
else:
- logger.warn(
+ logger.warning(
"User %s not authorized to view notif_id %s belonging to user %s",
request.user,
notif_id, notif.user
diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py
index 79c34d14..f777aed4 100644
--- a/allianceauth/project_template/project_name/settings/base.py
+++ b/allianceauth/project_template/project_name/settings/base.py
@@ -41,23 +41,23 @@ CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = {
'esi_cleanup_callbackredirect': {
'task': 'esi.tasks.cleanup_callbackredirect',
- 'schedule': crontab(minute=0, hour='*/4'),
+ 'schedule': crontab(minute='0', hour='*/4'),
},
'esi_cleanup_token': {
'task': 'esi.tasks.cleanup_token',
- 'schedule': crontab(minute=0, hour=0),
+ 'schedule': crontab(minute='0', hour='0'),
},
'run_model_update': {
'task': 'allianceauth.eveonline.tasks.run_model_update',
- 'schedule': crontab(minute=0, hour="*/6"),
+ 'schedule': crontab(minute='0', hour="*/6"),
},
'check_all_character_ownership': {
'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
- 'schedule': crontab(minute=0, hour='*/4'),
+ 'schedule': crontab(minute='0', hour='*/4'),
},
'analytics_daily_stats': {
'task': 'allianceauth.analytics.tasks.analytics_daily_stats',
- 'schedule': crontab(minute=0, hour=2),
+ 'schedule': crontab(minute='0', hour='2'),
}
}
diff --git a/allianceauth/project_template/project_name/settings/local.py b/allianceauth/project_template/project_name/settings/local.py
index 296bee02..d83fd718 100644
--- a/allianceauth/project_template/project_name/settings/local.py
+++ b/allianceauth/project_template/project_name/settings/local.py
@@ -32,6 +32,13 @@ INSTALLED_APPS += [
# To change the logging level for extensions, uncomment the following line.
# LOGGING['handlers']['extension_file']['level'] = 'DEBUG'
+# By default, apps are prevented from having public views for security reasons.
+# To allow specific apps to have public views, add them to APPS_WITH_PUBLIC_VIEWS
+# » The format is the same as in INSTALLED_APPS
+# » The app developer must also explicitly allow public views for their app
+APPS_WITH_PUBLIC_VIEWS = [
+
+]
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
DATABASES['default'] = {
diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py
index 789bc6bc..862c7339 100644
--- a/allianceauth/services/hooks.py
+++ b/allianceauth/services/hooks.py
@@ -1,15 +1,18 @@
-from django.urls import include
-from django.urls import re_path
+from django.urls import include, re_path
+from string import Formatter
+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 django.conf import settings
-from string import Formatter
from allianceauth.hooks import get_hooks
from .models import NameFormatConfig
+
def get_extension_logger(name):
"""
Takes the name of a plugin/extension and generates a child logger of the extensions logger
@@ -156,8 +159,32 @@ class MenuItemHook:
class UrlHook:
- def __init__(self, urls, namespace, base_url):
+ """A hook for registering the URLs of a Django app.
+
+ Args:
+ - urls: The urls module to include
+ - namespace: The URL namespace to apply. This is usually just the app name.
+ - base_url: The URL prefix to match against in regex form.
+ Example ``r'^app_name/'``.
+ This prefix will be applied in front of all URL patterns included.
+ It is possible to use the same prefix as existing apps (or no prefix at all),
+ but standard URL resolution ordering applies
+ (hook URLs are the last ones registered).
+ - excluded_views: Optional list of views to be excluded
+ from auto-decorating them with the
+ default ``main_character_required`` decorator, e.g. to make them public.
+ Views must be specified by their qualified name,
+ e.g. ``["example.views.my_public_view"]``
+ """
+ def __init__(
+ self,
+ urls,
+ namespace: str,
+ base_url: str,
+ excluded_views : Optional[Iterable[str]] = None
+ ):
self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
+ self.excluded_views = set(excluded_views or [])
class NameFormatter:
diff --git a/allianceauth/services/modules/discord/discord_client/client.py b/allianceauth/services/modules/discord/discord_client/client.py
index 65da9e0c..048088e3 100644
--- a/allianceauth/services/modules/discord/discord_client/client.py
+++ b/allianceauth/services/modules/discord/discord_client/client.py
@@ -588,16 +588,17 @@ class DiscordClient:
return None # User is no longer a member
guild_roles = RolesSet(self.guild_roles(guild_id=guild_id))
logger.debug('Current guild roles: %s', guild_roles.ids())
+ _roles = set(member_info.roles)
if not guild_roles.has_roles(member_info.roles):
guild_roles = RolesSet(
self.guild_roles(guild_id=guild_id, use_cache=False)
)
if not guild_roles.has_roles(member_info.roles):
role_ids = set(member_info.roles).difference(guild_roles.ids())
- raise RuntimeError(
- f'Discord user {user_id} has unknown roles: {role_ids}'
- )
- return guild_roles.subset(member_info.roles)
+ logger.warning(f'Discord user {user_id} has unknown roles: {role_ids}')
+ for _r in role_ids:
+ _roles.remove(_r)
+ return guild_roles.subset(_roles)
@classmethod
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
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 adf22323..5c7a5bf4 100644
--- a/allianceauth/services/modules/discord/discord_client/tests/test_client.py
+++ b/allianceauth/services/modules/discord/discord_client/tests/test_client.py
@@ -899,8 +899,8 @@ class TestGuildMemberRoles(NoSocketsTestCase):
mock_guild_roles.return_value = {role_a, role_b}
client = DiscordClientStub(TEST_BOT_TOKEN, mock_redis)
# when/then
- with self.assertRaises(RuntimeError):
- client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
+ roles = client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
+ self.assertEqual(roles, RolesSet([role_a]))
# TODO: Re-enable after adding Discord general error handling
# def test_should_raise_exception_if_member_info_is_invalid(
diff --git a/allianceauth/services/modules/discord/tests/piloting_tasks.py b/allianceauth/services/modules/discord/tests/piloting_tasks.py
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/discord/urls.py b/allianceauth/services/modules/discord/urls.py
index de82a91c..4685a3c5 100644
--- a/allianceauth/services/modules/discord/urls.py
+++ b/allianceauth/services/modules/discord/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/discourse/tasks.py b/allianceauth/services/modules/discourse/tasks.py
index d3a542c8..d115211a 100644
--- a/allianceauth/services/modules/discourse/tasks.py
+++ b/allianceauth/services/modules/discourse/tasks.py
@@ -49,7 +49,7 @@ class DiscourseTasks:
DiscourseManager.update_groups(user)
except Exception as e:
logger.exception(e)
- logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
+ logger.warning("Discourse group sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
logger.debug("Updated user %s discourse groups." % user)
diff --git a/allianceauth/services/modules/example/urls.py b/allianceauth/services/modules/example/urls.py
index c8a427e5..a08be65f 100644
--- a/allianceauth/services/modules/example/urls.py
+++ b/allianceauth/services/modules/example/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
app_name = 'example'
diff --git a/allianceauth/services/modules/ips4/urls.py b/allianceauth/services/modules/ips4/urls.py
index a11bbd1b..cb8aaa4b 100644
--- a/allianceauth/services/modules/ips4/urls.py
+++ b/allianceauth/services/modules/ips4/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/mumble/urls.py b/allianceauth/services/modules/mumble/urls.py
index dfa77c79..ef7ff699 100644
--- a/allianceauth/services/modules/mumble/urls.py
+++ b/allianceauth/services/modules/mumble/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/openfire/manager.py b/allianceauth/services/modules/openfire/manager.py
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html b/allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/openfire/urls.py b/allianceauth/services/modules/openfire/urls.py
index 99ce6713..17da5b33 100644
--- a/allianceauth/services/modules/openfire/urls.py
+++ b/allianceauth/services/modules/openfire/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/phpbb3/manager.py b/allianceauth/services/modules/phpbb3/manager.py
old mode 100755
new mode 100644
index c62417b1..7e6120a6
--- a/allianceauth/services/modules/phpbb3/manager.py
+++ b/allianceauth/services/modules/phpbb3/manager.py
@@ -176,7 +176,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.warn("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
+ logger.warning("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
Phpbb3Manager.__update_user_info(username_clean, email, pwhash)
else:
try:
diff --git a/allianceauth/services/modules/phpbb3/urls.py b/allianceauth/services/modules/phpbb3/urls.py
index 5fcaba55..e9dcbd2a 100644
--- a/allianceauth/services/modules/phpbb3/urls.py
+++ b/allianceauth/services/modules/phpbb3/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/smf/urls.py b/allianceauth/services/modules/smf/urls.py
index 1dedc8d2..3f8308f5 100644
--- a/allianceauth/services/modules/smf/urls.py
+++ b/allianceauth/services/modules/smf/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/teamspeak3/manager.py b/allianceauth/services/modules/teamspeak3/manager.py
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/teamspeak3/urls.py b/allianceauth/services/modules/teamspeak3/urls.py
index 92c3093e..83cc4287 100644
--- a/allianceauth/services/modules/teamspeak3/urls.py
+++ b/allianceauth/services/modules/teamspeak3/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/modules/teamspeak3/util/__init__.py b/allianceauth/services/modules/teamspeak3/util/__init__.py
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/teamspeak3/util/ts3.py b/allianceauth/services/modules/teamspeak3/util/ts3.py
old mode 100755
new mode 100644
diff --git a/allianceauth/services/modules/teamspeak3/views.py b/allianceauth/services/modules/teamspeak3/views.py
index 3d1ff5e2..e54bd84b 100644
--- a/allianceauth/services/modules/teamspeak3/views.py
+++ b/allianceauth/services/modules/teamspeak3/views.py
@@ -44,7 +44,7 @@ def activate_teamspeak3(request):
def verify_teamspeak3(request):
logger.debug("verify_teamspeak3 called by user %s" % request.user)
if not Teamspeak3Tasks.has_account(request.user):
- logger.warn("Unable to validate user %s teamspeak: no teamspeak data" % request.user)
+ logger.warning("Unable to validate user %s teamspeak: no teamspeak data" % request.user)
return redirect("services:services")
if request.method == "POST":
form = TeamspeakJoinForm(request.POST)
diff --git a/allianceauth/services/modules/xenforo/urls.py b/allianceauth/services/modules/xenforo/urls.py
index fe684e9f..bec8e6ef 100644
--- a/allianceauth/services/modules/xenforo/urls.py
+++ b/allianceauth/services/modules/xenforo/urls.py
@@ -1,5 +1,4 @@
-from django.urls import include
-from django.urls import path
+from django.urls import include, path
from . import views
diff --git a/allianceauth/services/templates/services/services.html b/allianceauth/services/templates/services/services.html
old mode 100755
new mode 100644
diff --git a/allianceauth/services/tests/test_hooks.py b/allianceauth/services/tests/test_hooks.py
new file mode 100644
index 00000000..dd3d787b
--- /dev/null
+++ b/allianceauth/services/tests/test_hooks.py
@@ -0,0 +1,30 @@
+from unittest import TestCase
+
+from allianceauth.services.hooks import UrlHook
+from allianceauth.groupmanagement import urls
+
+
+class TestUrlHook(TestCase):
+ def test_can_create_simple_hook(self):
+ # when
+ obj = UrlHook(urls, "groupmanagement", r"^groupmanagement/")
+ # then
+ self.assertEqual(obj.include_pattern.app_name, "groupmanagement")
+ self.assertFalse(obj.excluded_views)
+
+ def test_can_create_hook_with_excluded_views(self):
+ # when
+ obj = UrlHook(
+ urls,
+ "groupmanagement",
+ r"^groupmanagement/",
+ ["groupmanagement.views.group_management"],
+ )
+ # then
+ self.assertEqual(obj.include_pattern.app_name, "groupmanagement")
+ self.assertIn("groupmanagement.views.group_management", obj.excluded_views)
+
+ def test_should_raise_error_when_called_with_invalid_excluded_views(self):
+ # when/then
+ with self.assertRaises(TypeError):
+ UrlHook(urls, "groupmanagement", r"^groupmanagement/", 99)
diff --git a/allianceauth/services/views.py b/allianceauth/services/views.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/__init__.py b/allianceauth/srp/__init__.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/admin.py b/allianceauth/srp/admin.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/form.py b/allianceauth/srp/form.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/models.py b/allianceauth/srp/models.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/tests/__init__.py b/allianceauth/srp/tests/__init__.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/tests/test_managers.py b/allianceauth/srp/tests/test_managers.py
old mode 100755
new mode 100644
diff --git a/allianceauth/srp/views.py b/allianceauth/srp/views.py
old mode 100755
new mode 100644
diff --git a/allianceauth/static/allianceauth/css/auth-base.css b/allianceauth/static/allianceauth/css/auth-base.css
index 6eb51d8e..b2b18aa5 100644
--- a/allianceauth/static/allianceauth/css/auth-base.css
+++ b/allianceauth/static/allianceauth/css/auth-base.css
@@ -122,7 +122,7 @@ ul.list-group.list-group-horizontal > li.list-group-item {
padding-top: 0.5rem;
}
- .navbar-nav > li.top-user-menu.with-main-character a {
+ .navbar-nav > li.top-user-menu a {
padding: 14px;
}
diff --git a/allianceauth/static/allianceauth/icons/android-chrome-192x192.png b/allianceauth/static/allianceauth/icons/android-chrome-192x192.png
new file mode 100644
index 00000000..a83b4d06
Binary files /dev/null and b/allianceauth/static/allianceauth/icons/android-chrome-192x192.png differ
diff --git a/allianceauth/static/allianceauth/icons/android-chrome-512x512.png b/allianceauth/static/allianceauth/icons/android-chrome-512x512.png
new file mode 100644
index 00000000..2cdf71f0
Binary files /dev/null and b/allianceauth/static/allianceauth/icons/android-chrome-512x512.png differ
diff --git a/allianceauth/static/allianceauth/icons/apple-touch-icon.png b/allianceauth/static/allianceauth/icons/apple-touch-icon.png
old mode 100755
new mode 100644
index f1d16af3..7c7ea0f6
Binary files a/allianceauth/static/allianceauth/icons/apple-touch-icon.png and b/allianceauth/static/allianceauth/icons/apple-touch-icon.png differ
diff --git a/allianceauth/static/allianceauth/icons/browserconfig.xml b/allianceauth/static/allianceauth/icons/browserconfig.xml
new file mode 100644
index 00000000..1677cc2c
--- /dev/null
+++ b/allianceauth/static/allianceauth/icons/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+ {% blocktranslate with running_count=tasks_running|default_if_none:"?"|intcomma %} + {{ running_count }} running | + {% endblocktranslate %} {% blocktranslate with queue_length=task_queue_length|default_if_none:"?"|intcomma %} - {{ queue_length }} queued tasks + {{ queue_length }} queued {% endblocktranslate %}
diff --git a/allianceauth/templates/allianceauth/base.html b/allianceauth/templates/allianceauth/base.html index 2becf3c2..f82e7124 100644 --- a/allianceauth/templates/allianceauth/base.html +++ b/allianceauth/templates/allianceauth/base.html @@ -21,33 +21,37 @@ - {% if user.is_authenticated %} -