Merge branch 'master' of gitlab.com:allianceauth/allianceauth into v4.x
@ -1,6 +0,0 @@
|
||||
[settings]
|
||||
profile=django
|
||||
sections=FUTURE,STDLIB,THIRDPARTY,DJANGO,ESI,FIRSTPARTY,LOCALFOLDER
|
||||
known_esi=esi
|
||||
known_django=django
|
||||
skip_gitignore=true
|
@ -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]
|
||||
|
0
.tx/config_20230406134150.bak
Executable file → Normal file
@ -1,7 +0,0 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include MANIFEST.in
|
||||
graft allianceauth
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
@ -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__}'
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
|
0
allianceauth/authentication/managers.py
Executable file → Normal file
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
29
allianceauth/authentication/models.py
Executable file → Normal file
@ -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(
|
||||
|
@ -1,18 +1,26 @@
|
||||
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:
|
||||
@ -23,13 +31,14 @@ def dashboard_results(hours: int) -> _TaskCounts:
|
||||
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,
|
||||
)
|
||||
|
@ -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."""
|
||||
@ -101,9 +71,9 @@ class EventSeries:
|
||||
- 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,
|
||||
|
108
allianceauth/authentication/task_statistics/helpers.py
Normal file
@ -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
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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"
|
||||
|
@ -6,7 +6,7 @@
|
||||
{% block page_title %}{% translate "Login" %}{% endblock %}
|
||||
|
||||
{% block middle_box_content %}
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next | urlencode}}{%endif%}">
|
||||
<img class="img-responsive center-block" src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" alt="{% translate 'Login with Eve SSO' %}">
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
@ -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)
|
||||
|
0
allianceauth/eveonline/views.py
Executable file → Normal file
0
allianceauth/groupmanagement/views.py
Executable file → Normal file
0
allianceauth/hrapplications/admin.py
Executable file → Normal file
0
allianceauth/hrapplications/forms.py
Executable file → Normal file
0
allianceauth/hrapplications/models.py
Executable file → Normal file
18
allianceauth/hrapplications/views.py
Executable file → Normal file
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'] = {
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
0
allianceauth/services/modules/discord/tests/piloting_tasks.py
Executable file → Normal file
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = 'example'
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
0
allianceauth/services/modules/openfire/manager.py
Executable file → Normal file
0
allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html
Executable file → Normal file
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
2
allianceauth/services/modules/phpbb3/manager.py
Executable file → Normal file
@ -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:
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
0
allianceauth/services/modules/teamspeak3/manager.py
Executable file → Normal file
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
0
allianceauth/services/modules/teamspeak3/util/__init__.py
Executable file → Normal file
0
allianceauth/services/modules/teamspeak3/util/ts3.py
Executable file → Normal file
@ -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)
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
0
allianceauth/services/templates/services/services.html
Executable file → Normal file
30
allianceauth/services/tests/test_hooks.py
Normal file
@ -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)
|
0
allianceauth/services/views.py
Executable file → Normal file
0
allianceauth/srp/__init__.py
Executable file → Normal file
0
allianceauth/srp/admin.py
Executable file → Normal file
0
allianceauth/srp/form.py
Executable file → Normal file
0
allianceauth/srp/models.py
Executable file → Normal file
0
allianceauth/srp/tests/__init__.py
Executable file → Normal file
0
allianceauth/srp/tests/test_managers.py
Executable file → Normal file
0
allianceauth/srp/views.py
Executable file → Normal file
@ -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;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 26 KiB |
BIN
allianceauth/static/allianceauth/icons/apple-touch-icon.png
Executable file → Normal file
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.0 KiB |
9
allianceauth/static/allianceauth/icons/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/static/allianceauth/icons/mstile-150x150.png"/>
|
||||
<TileColor>#2d89ef</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
allianceauth/static/allianceauth/icons/favicon-16x16.png
Executable file → Normal file
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 941 B |
BIN
allianceauth/static/allianceauth/icons/favicon-32x32.png
Executable file → Normal file
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 1.5 KiB |
BIN
allianceauth/static/allianceauth/icons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
allianceauth/static/allianceauth/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
41
allianceauth/static/allianceauth/icons/safari-pinned-tab.svg
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="575.000000pt" height="575.000000pt" viewBox="0 0 575.000000 575.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,575.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2765 5731 c-58 -21 -122 -62 -143 -92 -8 -11 -32 -62 -54 -112 -21
|
||||
-51 -45 -105 -53 -122 -7 -16 -47 -109 -88 -205 -112 -261 -375 -871 -415
|
||||
-960 -11 -25 -61 -142 -112 -260 -50 -118 -102 -237 -115 -265 -12 -27 -71
|
||||
-162 -130 -300 -217 -505 -297 -691 -310 -720 -8 -16 -145 -334 -305 -705
|
||||
-160 -371 -298 -691 -307 -710 -28 -63 -40 -91 -168 -390 -70 -162 -133 -308
|
||||
-140 -325 -8 -16 -25 -55 -38 -85 -13 -30 -35 -80 -47 -110 -12 -30 -31 -73
|
||||
-41 -95 -16 -35 -36 -81 -106 -244 l-14 -33 33 20 c18 11 103 60 188 110 85
|
||||
49 171 99 190 111 19 11 55 32 80 46 25 14 70 40 101 58 208 121 326 189 339
|
||||
198 8 5 94 55 190 110 96 56 191 111 210 122 19 11 46 27 60 34 14 8 46 26 71
|
||||
41 69 41 557 326 577 337 9 6 68 39 130 75 62 36 125 71 140 79 15 8 29 18 30
|
||||
23 2 4 8 8 13 8 4 0 46 22 91 49 46 27 99 58 118 68 19 11 172 100 340 198
|
||||
168 98 320 186 338 197 18 10 103 59 187 108 84 49 169 98 187 108 18 11 56
|
||||
33 84 49 27 17 70 41 95 55 24 14 123 72 219 128 96 56 195 114 220 128 25 13
|
||||
50 29 57 36 9 8 -16 74 -103 276 -63 146 -121 279 -128 295 -8 17 -28 64 -46
|
||||
105 -34 80 -55 127 -75 175 -71 163 -148 341 -355 820 -133 308 -250 578 -260
|
||||
600 -10 22 -44 101 -75 175 -32 74 -66 153 -76 175 -10 22 -65 148 -121 281
|
||||
-113 263 -126 282 -224 326 -67 29 -173 34 -239 9z"/>
|
||||
<path d="M4500 2370 c-14 -11 -28 -20 -32 -20 -6 0 -107 -58 -158 -90 -8 -5
|
||||
-28 -17 -45 -26 -16 -9 -168 -97 -337 -195 -169 -99 -322 -187 -340 -198 -18
|
||||
-10 -58 -33 -88 -51 -30 -18 -71 -42 -90 -53 -44 -25 -414 -239 -418 -242 -2
|
||||
-2 16 -14 40 -27 24 -14 153 -89 288 -168 135 -78 259 -150 275 -160 17 -9
|
||||
116 -67 220 -127 105 -61 206 -120 225 -130 19 -11 86 -50 148 -87 62 -36 132
|
||||
-77 155 -90 23 -13 98 -56 167 -96 68 -40 138 -81 155 -90 16 -9 64 -37 105
|
||||
-62 41 -24 82 -47 90 -51 8 -4 29 -15 45 -26 151 -92 639 -370 643 -366 3 3
|
||||
-1 17 -8 32 -7 16 -56 129 -110 253 -93 216 -131 302 -150 345 -5 11 -62 142
|
||||
-126 290 -64 149 -124 288 -134 310 -10 22 -39 90 -64 150 -26 61 -54 124 -61
|
||||
141 -8 16 -30 68 -50 114 -78 183 -84 197 -124 283 -17 37 -31 74 -31 80 0 7
|
||||
-4 17 -9 22 -4 6 -41 87 -81 180 -40 94 -73 171 -74 172 -1 1 -12 -7 -26 -17z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
19
allianceauth/static/allianceauth/icons/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/allianceauth/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/allianceauth/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
30
allianceauth/static/allianceauth/js/timerboard.js
Normal file
@ -0,0 +1,30 @@
|
||||
$(document).ready(() => {
|
||||
'use strict';
|
||||
const inputAbsoluteTime = $('input#id_absolute_time');
|
||||
const inputCountdown = $('#id_days_left, #id_hours_left, #id_minutes_left');
|
||||
|
||||
//inputAbsoluteTime.prop('disabled', true);
|
||||
inputAbsoluteTime.parent().hide()
|
||||
inputAbsoluteTime.parent().prev('label').hide()
|
||||
inputCountdown.prop('required', true);
|
||||
|
||||
$('input#id_absolute_checkbox').change(function () {
|
||||
if ($(this).prop("checked")) {
|
||||
// check box enabled
|
||||
inputAbsoluteTime.parent().show()
|
||||
inputAbsoluteTime.parent().prev('label').show()
|
||||
inputCountdown.parent().hide()
|
||||
inputCountdown.parent().prev('label').hide()
|
||||
inputAbsoluteTime.prop('required', true);
|
||||
inputCountdown.prop('required', false);
|
||||
} else {
|
||||
// Checkbox is not checked
|
||||
inputAbsoluteTime.parent().hide()
|
||||
inputAbsoluteTime.parent().prev('label').hide()
|
||||
inputCountdown.parent().show()
|
||||
inputCountdown.parent().prev('label').show()
|
||||
inputAbsoluteTime.prop('required', false);
|
||||
inputCountdown.prop('required', true);
|
||||
}
|
||||
});
|
||||
});
|
@ -92,8 +92,11 @@
|
||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
|
||||
</div>
|
||||
<p>
|
||||
{% 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 %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -21,25 +21,28 @@
|
||||
</head>
|
||||
|
||||
<body class="{% if NIGHT_MODE %}template-dark-mode{% else %}template-light-mode{% endif %}">
|
||||
{% if user.is_authenticated %}
|
||||
<div id="wrapper" class="container">
|
||||
<!-- Navigation -->
|
||||
{% include 'allianceauth/top-menu.html' %}
|
||||
<div class="row" id="site-body-wrapper">
|
||||
|
||||
<div class="clearfix{% if user.is_authenticated %} row{% endif %}" id="site-body-wrapper">
|
||||
{% if user.is_authenticated %}
|
||||
{% include 'allianceauth/side-menu.html' %}
|
||||
<div class="col-sm-10">
|
||||
{% endif %}
|
||||
|
||||
<div class="{% if user.is_authenticated %}col-sm-10{% else %}col-sm-12{% endif %}">
|
||||
{% include 'allianceauth/messages.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% include 'bundles/jquery-visibility-js.html' %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<script>
|
||||
let notificationUPdateSettings = {
|
||||
notificationsListViewUrl: "{% url 'notifications:list' %}",
|
||||
@ -48,6 +51,7 @@
|
||||
};
|
||||
</script>
|
||||
{% include 'bundles/refresh-notifications-js.html' %}
|
||||
{% endif %}
|
||||
{% include 'bundles/evetime-js.html' %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
|
@ -1,5 +1,12 @@
|
||||
{% load static %}
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-16x16.png' %}" sizes="16x16">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-32x32.png' %}" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-96x96.png' %}" sizes="96x96">
|
||||
<link rel="apple-touch-icon" href="{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<link rel="manifest" href="{% static 'allianceauth/icons/site.webmanifest' %}">
|
||||
<link rel="mask-icon" href="{% static 'allianceauth/icons/safari-pinned-tab.svg' %}" color="#5bbad5">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'allianceauth/icons/favicon.ico' %}">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="msapplication-config" content="{% static 'allianceauth/icons/browserconfig.xml' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
@ -11,7 +11,10 @@
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<img class="img-rounded ra-avatar" src="{{ 1|character_portrait_url:32 }}" alt="{{ main.character_name }}">
|
||||
<span class="hidden-sm hidden-md hidden-lg">
|
||||
{% translate "User Menu" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
|
@ -22,9 +22,12 @@
|
||||
<li class="nav-item-eve-time">
|
||||
<div class="eve-time-wrapper">{% translate "Eve Time" %}: <span class="eve-time-clock"></span></div>
|
||||
</li>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<li class="{% navactive request 'notifications:' %}" id="menu_item_notifications">
|
||||
{% include 'allianceauth/notifications_menu_item.html' %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% include 'allianceauth/top-menu-user-dropdown.html' %}
|
||||
</ul>
|
||||
|
3
allianceauth/templates/bundles/timerboard-js.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% load static %}
|
||||
|
||||
<script src="{% static 'allianceauth/js/timerboard.js' %}"></script>
|
@ -54,7 +54,8 @@ def status_overview() -> dict:
|
||||
"tasks_failed": 0,
|
||||
"tasks_total": 0,
|
||||
"tasks_hours": 0,
|
||||
"earliest_task": None
|
||||
"earliest_task": None,
|
||||
"tasks_running": 0
|
||||
}
|
||||
response.update(_current_notifications())
|
||||
response.update(_current_version_summary())
|
||||
@ -72,7 +73,8 @@ def _celery_stats() -> dict:
|
||||
"tasks_failed": results.failed,
|
||||
"tasks_total": results.total,
|
||||
"tasks_hours": results.hours,
|
||||
"earliest_task": results.earliest_task
|
||||
"earliest_task": results.earliest_task,
|
||||
"tasks_running": results.running,
|
||||
}
|
||||
|
||||
|
||||
|
72
allianceauth/tests/test_urls.py
Normal file
@ -0,0 +1,72 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.urls import URLPattern
|
||||
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
from allianceauth.urls import urls_from_apps
|
||||
|
||||
MODULE_PATH = "allianceauth.urls"
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".main_character_required")
|
||||
@patch(MODULE_PATH + ".decorate_url_patterns")
|
||||
class TestUrlsFromApps(TestCase):
|
||||
def test_should_decorate_url_by_default(self, mock_decorate_url_patterns, mock_main_character_required):
|
||||
# given
|
||||
def hook_function():
|
||||
return UrlHook(urlconf_module, "my_namespace", r"^my_app/")
|
||||
|
||||
view = MagicMock(name="view")
|
||||
path = MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
urlconf_module = [patch], "my_app"
|
||||
# when
|
||||
result = urls_from_apps([hook_function], [])
|
||||
# then
|
||||
self.assertIsInstance(result[0], URLPattern)
|
||||
self.assertTrue(mock_decorate_url_patterns.called)
|
||||
args, _ = mock_decorate_url_patterns.call_args
|
||||
decorator = args[1]
|
||||
self.assertEqual(decorator, mock_main_character_required)
|
||||
excluded_views = args[2]
|
||||
self.assertIsNone(excluded_views)
|
||||
|
||||
def test_should_not_decorate_when_excluded(self, mock_decorate_url_patterns, mock_main_character_required):
|
||||
# given
|
||||
def hook_function():
|
||||
return UrlHook(urlconf_module, "my_namespace", r"^my_app/", ["excluded_view"])
|
||||
|
||||
view = MagicMock(name="view")
|
||||
path = MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
urlconf_module = [patch], "my_app"
|
||||
# when
|
||||
result = urls_from_apps([hook_function], ["my_app"])
|
||||
# then
|
||||
self.assertIsInstance(result[0], URLPattern)
|
||||
self.assertTrue(mock_decorate_url_patterns.called)
|
||||
args, _ = mock_decorate_url_patterns.call_args
|
||||
decorator = args[1]
|
||||
self.assertEqual(decorator, mock_main_character_required)
|
||||
excluded_views = args[2]
|
||||
self.assertSetEqual(excluded_views, {"excluded_view"})
|
||||
|
||||
def test_should_decorate_when_app_has_no_permission(self, mock_decorate_url_patterns, mock_main_character_required):
|
||||
# given
|
||||
def hook_function():
|
||||
return UrlHook(urlconf_module, "my_namespace", r"^my_app/", ["excluded_view"])
|
||||
|
||||
view = MagicMock(name="view")
|
||||
path = MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
urlconf_module = [patch], "my_app"
|
||||
# when
|
||||
result = urls_from_apps([hook_function], ["other_app"])
|
||||
# then
|
||||
self.assertIsInstance(result[0], URLPattern)
|
||||
self.assertTrue(mock_decorate_url_patterns.called)
|
||||
args, _ = mock_decorate_url_patterns.call_args
|
||||
decorator = args[1]
|
||||
self.assertEqual(decorator, mock_main_character_required)
|
||||
excluded_views = args[2]
|
||||
self.assertIsNone(excluded_views)
|
0
allianceauth/timerboard/__init__.py
Executable file → Normal file
0
allianceauth/timerboard/admin.py
Executable file → Normal file
31
allianceauth/timerboard/form.py
Executable file → Normal file
@ -61,14 +61,17 @@ class TimerForm(forms.ModelForm):
|
||||
structure = forms.ChoiceField(choices=structure_choices, required=True, label=_("Structure Type"))
|
||||
timer_type = forms.ChoiceField(choices=TimerType.choices, label=_("Timer Type"))
|
||||
objective = forms.ChoiceField(choices=objective_choices, required=True, label=_("Objective"))
|
||||
days_left = forms.IntegerField(required=True, label=_("Days Remaining"), validators=[MinValueValidator(0)])
|
||||
hours_left = forms.IntegerField(required=True, label=_("Hours Remaining"),
|
||||
absolute_checkbox = forms.BooleanField(label=_("Absolute Timer"), required=False, initial=False)
|
||||
absolute_time = forms.CharField(required=False,label=_("Date and Time"))
|
||||
days_left = forms.IntegerField(required=False, label=_("Days Remaining"), validators=[MinValueValidator(0)])
|
||||
hours_left = forms.IntegerField(required=False, label=_("Hours Remaining"),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(23)])
|
||||
minutes_left = forms.IntegerField(required=True, label=_("Minutes Remaining"),
|
||||
minutes_left = forms.IntegerField(required=False, label=_("Minutes Remaining"),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(59)])
|
||||
important = forms.BooleanField(label=_("Important"), required=False)
|
||||
corp_timer = forms.BooleanField(label=_("Corp-Restricted"), required=False)
|
||||
|
||||
|
||||
def save(self, commit=True):
|
||||
timer = super().save(commit=False)
|
||||
|
||||
@ -77,18 +80,30 @@ class TimerForm(forms.ModelForm):
|
||||
corporation = character.corporation
|
||||
logger.debug("Determined timer save request on behalf "
|
||||
"of character {} corporation {}".format(character, corporation))
|
||||
# calculate future time
|
||||
future_time = datetime.timedelta(days=self.cleaned_data['days_left'], hours=self.cleaned_data['hours_left'],
|
||||
minutes=self.cleaned_data['minutes_left'])
|
||||
|
||||
days_left = self.cleaned_data['days_left']
|
||||
hours_left = self.cleaned_data['hours_left']
|
||||
minutes_left = self.cleaned_data['minutes_left']
|
||||
absolute_time = self.cleaned_data['absolute_time']
|
||||
|
||||
if days_left or hours_left or minutes_left:
|
||||
# Calculate future time
|
||||
future_time = datetime.timedelta(days=days_left, hours=hours_left, minutes=minutes_left)
|
||||
current_time = timezone.now()
|
||||
eve_time = current_time + future_time
|
||||
logger.debug(
|
||||
f"Determined timer eve time is {eve_time} - current time {current_time}, adding {future_time}")
|
||||
logger.debug(f"Determined timer eve time is {eve_time} - current time {current_time}, adding {future_time}")
|
||||
elif absolute_time:
|
||||
# Use absolute time
|
||||
eve_time = absolute_time
|
||||
else:
|
||||
raise ValueError("Either future time or absolute time must be provided.")
|
||||
|
||||
timer.eve_time = eve_time
|
||||
timer.eve_character = character
|
||||
timer.eve_corp = corporation
|
||||
timer.user = self.user
|
||||
|
||||
if commit:
|
||||
timer.save()
|
||||
|
||||
return timer
|
||||
|
0
allianceauth/timerboard/models.py
Executable file → Normal file
@ -30,3 +30,21 @@
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/jquery-datetimepicker-js.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$('#id_start').datetimepicker({
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
{% if NIGHT_MODE %}
|
||||
theme: 'dark',
|
||||
{% else %}
|
||||
theme: 'default',
|
||||
{% endif %}
|
||||
mask: true,
|
||||
format: 'Y-m-d H:i',
|
||||
minDate: 0
|
||||
});
|
||||
{% endblock extra_script %}
|
||||
|
@ -12,3 +12,19 @@
|
||||
{% block submit_button_text %}
|
||||
{% translate "Create Timer" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/timerboard-js.html' %}
|
||||
{% include 'bundles/jquery-datetimepicker-js.html' %}
|
||||
{% include 'bundles/jquery-datetimepicker-css.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$('input#id_absolute_time').datetimepicker({
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
theme: {% if NIGHT_MODE %}'dark'{% else %}'default'{% endif %},
|
||||
format: 'Y-m-d H:i',
|
||||
minDate: 0,
|
||||
defaultDate: null
|
||||
});
|
||||
{% endblock extra_script %}
|
||||
|
@ -12,3 +12,23 @@
|
||||
{% block submit_button_text %}
|
||||
{% translate "Update Structure Timer" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/timerboard-js.html' %}
|
||||
{% include 'bundles/jquery-datetimepicker-js.html' %}
|
||||
{% include 'bundles/jquery-datetimepicker-css.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
$('input#id_absolute_time').datetimepicker({
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
{% if NIGHT_MODE %}
|
||||
theme: 'dark',
|
||||
{% else %}
|
||||
theme: 'default',
|
||||
{% endif %}
|
||||
mask: true,
|
||||
format: 'Y-m-d H:i',
|
||||
defaultDate: null
|
||||
});
|
||||
{% endblock extra_script %}
|
||||
|
0
allianceauth/timerboard/views.py
Executable file → Normal file
53
allianceauth/urls.py
Executable file → Normal file
@ -1,24 +1,55 @@
|
||||
from django.urls import path
|
||||
import esi.urls
|
||||
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 django.urls import URLPattern, include, path
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
import allianceauth.authentication.views
|
||||
import allianceauth.authentication.urls
|
||||
import allianceauth.notifications.urls
|
||||
import allianceauth.authentication.views
|
||||
import allianceauth.groupmanagement.urls
|
||||
import allianceauth.notifications.urls
|
||||
import allianceauth.services.urls
|
||||
from allianceauth.authentication.decorators import main_character_required, decorate_url_patterns
|
||||
from allianceauth import NAME
|
||||
from allianceauth import views
|
||||
from allianceauth import NAME, views
|
||||
from allianceauth.authentication import hmac_urls
|
||||
from allianceauth.authentication.decorators import (
|
||||
decorate_url_patterns,
|
||||
main_character_required
|
||||
)
|
||||
from allianceauth.hooks import get_hooks
|
||||
|
||||
admin.site.site_header = NAME
|
||||
|
||||
|
||||
def urls_from_apps(
|
||||
apps_hook_functions: Iterable[Callable], public_views_allowed: List[str]
|
||||
) -> List[URLPattern]:
|
||||
"""Return urls from apps and add default decorators."""
|
||||
|
||||
url_patterns = []
|
||||
allowed_apps = set(public_views_allowed)
|
||||
for app_hook_function in apps_hook_functions:
|
||||
url_hook = app_hook_function()
|
||||
app_pattern = url_hook.include_pattern
|
||||
excluded_views = (
|
||||
url_hook.excluded_views
|
||||
if app_pattern.app_name in allowed_apps
|
||||
else None
|
||||
)
|
||||
url_patterns += [
|
||||
path(
|
||||
"",
|
||||
decorate_url_patterns(
|
||||
[app_pattern], main_character_required, excluded_views
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
return url_patterns
|
||||
|
||||
|
||||
# Functional/Untranslated URL's
|
||||
urlpatterns = [
|
||||
# Locale
|
||||
@ -49,8 +80,6 @@ urlpatterns = [
|
||||
path('night/', views.NightModeRedirectView.as_view(), name='nightmode')
|
||||
]
|
||||
|
||||
|
||||
# Append app urls
|
||||
app_urls = get_hooks('url_hook')
|
||||
for app in app_urls:
|
||||
urlpatterns += [path('', decorate_url_patterns([app().include_pattern], main_character_required))]
|
||||
url_hooks = get_hooks("url_hook")
|
||||
public_views_allows = getattr(settings, "APPS_WITH_PUBLIC_VIEWS", [])
|
||||
urlpatterns += urls_from_apps(url_hooks, public_views_allows)
|
||||
|
@ -1,7 +1,7 @@
|
||||
PROTOCOL=https://
|
||||
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
|
||||
DOMAIN=%DOMAIN%
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v3.4.0
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v3.6.1
|
||||
|
||||
# Nginx Proxy Manager
|
||||
PROXY_HTTP_PORT=80
|
||||
@ -21,6 +21,7 @@ AA_DB_NAME=alliance_auth
|
||||
AA_DB_USER=aauth
|
||||
AA_DB_PASSWORD=%AA_DB_PASSWORD%
|
||||
AA_DB_ROOT_PASSWORD=%AA_DB_ROOT_PASSWORD%
|
||||
AA_DB_CHARSET=utf8mb4
|
||||
AA_EMAIL_HOST=''
|
||||
AA_EMAIL_PORT=587
|
||||
AA_EMAIL_HOST_USER=''
|
||||
|
@ -18,7 +18,7 @@ RUN mkdir -p ${STATIC_BASE} \
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
||||
libmariadb-dev gcc git
|
||||
libmariadb-dev gcc git pkg-config
|
||||
|
||||
# Install python dependencies
|
||||
RUN pip install --upgrade pip
|
||||
|
@ -17,6 +17,9 @@ DATABASES["default"] = {
|
||||
"PASSWORD": os.environ.get("AA_DB_PASSWORD"),
|
||||
"HOST": os.environ.get("AA_DB_HOST"),
|
||||
"PORT": os.environ.get("AA_DB_PORT", "3306"),
|
||||
"OPTIONS": {
|
||||
"charset": os.environ.get("AA_DB_CHARSET", "utf8mb4")
|
||||
}
|
||||
}
|
||||
|
||||
# Register an application at https://developers.eveonline.com for Authentication
|
||||
|
@ -115,7 +115,13 @@ services:
|
||||
depends_on:
|
||||
- auth_mysql
|
||||
volumes:
|
||||
- ./grafana-datasource.yml:/etc/grafana/provisioning/datasources/datasource.yaml
|
||||
- ./grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/datasource.yaml
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
environment:
|
||||
GF_INSTALL_PLUGINS: grafana-piechart-panel,grafana-clock-panel,grafana-simple-json-datasource
|
||||
GF_AUTH_DATABASE_PASSWORD: ${GRAFANA_DB_PASSWORD}
|
||||
|
||||
proxy:
|
||||
image: 'jc21/nginx-proxy-manager:latest'
|
||||
|
25
docker/grafana-dashboards.yml
Normal file
@ -0,0 +1,25 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
# <string> an unique provider name
|
||||
- name: 'auth dashboards'
|
||||
# <int> org id. will default to orgId 1 if not specified
|
||||
orgId: 1
|
||||
# <string, required> name of the dashboard folder. Required
|
||||
folder: ''
|
||||
# <string> folder UID. will be automatically generated if not specified
|
||||
folderUid: ''
|
||||
# <string, required> provider type. Required
|
||||
type: file
|
||||
# <bool> disable dashboard deletion
|
||||
disableDeletion: false
|
||||
# <bool> enable dashboard editing
|
||||
editable: true
|
||||
# <int> how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 10
|
||||
# <bool> allow updating provisioned dashboards from the UI
|
||||
allowUiUpdates: false
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: true
|
12
docker/grafana-datasource.yml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
|
||||
- name: MySQL
|
||||
type: mysql
|
||||
url: auth_mysql
|
||||
database: alliance_auth
|
||||
user: grafana
|
||||
editable: true
|
||||
secureJsonData:
|
||||
password: ${GF_AUTH_DATABASE_PASSWORD}
|
@ -71,3 +71,51 @@ urlpatterns = [
|
||||
re_path(r'', include(allianceauth.urls)),
|
||||
]
|
||||
```
|
||||
|
||||
## Example: Adding an external link to the sidebar
|
||||
|
||||
As an example we are adding an external links to the Alliance Auth sidebar using the template overrides feature. For our example let's add a link to Google's start page.
|
||||
|
||||
### Step 1 - Create the template override folder
|
||||
|
||||
First you need to create the folder for the template on your server. For AA to pick it up it has to match a specific structure.
|
||||
|
||||
If you have a default installation you can create the folder like this:
|
||||
|
||||
```sh
|
||||
mkdir -p /home/allianceserver/myauth/myauth/templates/allianceauth
|
||||
```
|
||||
|
||||
### Step 2 - Download the original template
|
||||
|
||||
Next you need to download a copy of the original template file we want to change. For that let's move into the above folder and then download the file into the current folder with:
|
||||
|
||||
```sh
|
||||
cd /home/allianceserver/myauth/myauth/templates/allianceauth
|
||||
wget <https://gitlab.com/allianceauth/allianceauth/-/raw/master/allianceauth/templates/allianceauth/side-menu.html>
|
||||
```
|
||||
|
||||
### Step 3 - Modify the template
|
||||
|
||||
Now you can modify the template to add your custom link. To create the google link we can add this snippet *between* the `{% menu_items %}` and the `</ul>` tag:
|
||||
|
||||
```sh
|
||||
nano /home/allianceserver/myauth/myauth/templates/allianceauth/side-menu.html
|
||||
```
|
||||
|
||||
```jinja
|
||||
<li>
|
||||
<a href="https://www.google.com/" target="_blank">
|
||||
<i class="fab fa-google fa-fw"></i>Google
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. hint::
|
||||
You can find other icons with a matching style on the `Font Awesome site <https://fontawesome.com/v5/search?m=free>`_ . AA currently uses Font Awesome version 5. You also want to keep the ``fa-fw`` tag to ensure all icons have the same width.
|
||||
```
|
||||
|
||||
### Step 4 - Restart your AA services
|
||||
|
||||
Finally, restart your AA services and your custom link should appear in the sidebar.
|
||||
|
@ -1,48 +1,51 @@
|
||||
# URL Hooks
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
URLs added through URL Hooks are protected by a decorator which ensures the requesting user is logged in and has a main character set.
|
||||
```
|
||||
## Base functionality
|
||||
|
||||
The URL hooks allow you to dynamically specify URL patterns from your plugin app or service. To achieve this you should subclass or instantiate the `services.hooks.UrlHook` class and then register the URL patterns with the hook.
|
||||
|
||||
To register a UrlHook class you would do the following:
|
||||
|
||||
```python
|
||||
@hooks.register('url_hook')
|
||||
def register_urls():
|
||||
return UrlHook(app_name.urls, 'app_name', r^'app_name/')
|
||||
```
|
||||
|
||||
### Public views
|
||||
|
||||
The `UrlHook` class specifies some parameters/instance variables required for URL pattern inclusion.
|
||||
In addition is it possible to make views public. Normally, all views are automatically decorated with the `main_character_required` decorator. That decorator ensures a user needs to be logged in and have a main before he can access that view. This feature protects against a community app sneaking in a public view without the administrator knowing about it.
|
||||
|
||||
`UrlHook(urls, app_name, base_url)`
|
||||
An app can opt-out of this feature by adding a list of views to be excluded when registering the URLs. See the `excluded_views` parameter for details.
|
||||
|
||||
#### urls
|
||||
The urls module to include. See [the Django docs](https://docs.djangoproject.com/en/dev/topics/http/urls/#example) for designing urlpatterns.
|
||||
#### 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](https://docs.djangoproject.com/en/dev/topics/http/urls/#how-django-processes-a-request) ordering applies (hook URLs are the last ones registered).
|
||||
```eval_rst
|
||||
.. note::
|
||||
Note that for a public view to work, administrators need to also explicitly allow apps to have public views in their AA installation, by adding the apps label to ``APPS_WITH_PUBLIC_VIEWS`` setting.
|
||||
```
|
||||
|
||||
### Example
|
||||
## Examples
|
||||
|
||||
An app called `plugin` provides a single view:
|
||||
|
||||
```python
|
||||
def index(request):
|
||||
return render(request, 'plugin/index.html')
|
||||
```
|
||||
|
||||
The app's `urls.py` would look like so:
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
import plugin.views
|
||||
|
||||
urlpatterns = [
|
||||
path('index/', plugins.views.index, name='index'),
|
||||
]
|
||||
```
|
||||
|
||||
Subsequently it would implement the UrlHook in a dedicated `auth_hooks.py` file like so:
|
||||
|
||||
```python
|
||||
from alliance_auth import hooks
|
||||
from services.hooks import UrlHook
|
||||
import plugin.urls
|
||||
@ -50,5 +53,13 @@ Subsequently it would implement the UrlHook in a dedicated `auth_hooks.py` file
|
||||
@hooks.register('url_hook')
|
||||
def register_urls():
|
||||
return UrlHook(plugin.urls, 'plugin', r^'plugin/')
|
||||
```
|
||||
|
||||
When this app is included in the project's `settings.INSTALLED_APPS` users would access the index view by navigating to `https://example.com/plugin/index`.
|
||||
|
||||
## API
|
||||
|
||||
```eval_rst
|
||||
.. autoclass:: allianceauth.services.hooks.UrlHook
|
||||
:members:
|
||||
```
|
||||
|
@ -82,6 +82,10 @@ Next we need to install Python and related development tools.
|
||||
.. note::
|
||||
Should your Ubuntu come with a newer version of Python we recommend to still setup your dev environment with the oldest Python 3 version currently supported by AA (e.g Python 3.8 at this time of writing) to ensure your apps are compatible with all current AA installations
|
||||
You an check out this `page <https://askubuntu.com/questions/682869/how-do-i-install-a-different-python-version-using-apt-get/1195153>`_ on how to install additional Python versions on Ubuntu.
|
||||
|
||||
If you install a different python version from the default you need to adjust some of the commands below to install appopriate versions of those packages for example using Python 3.8 you might need to run the following after using the setup steps for the repository mentioned in the AskUbuntu post above:
|
||||
|
||||
`sudo apt-get install python3.8 python3.8-dev python3.8-venv python3-setuptools python3-pip python-pip`
|
||||
```
|
||||
|
||||
Use the following command to install Python 3 with all required libraries with the default version:
|
||||
@ -93,7 +97,7 @@ sudo apt-get install python3 python3-dev python3-venv python3-setuptools python3
|
||||
### Install redis and other tools
|
||||
|
||||
```bash
|
||||
sudo apt-get install unzip git redis-server curl libssl-dev libbz2-dev libffi-dev
|
||||
sudo apt-get install unzip git redis-server curl libssl-dev libbz2-dev libffi-dev pkg-config
|
||||
```
|
||||
|
||||
Start redis
|
||||
|
@ -113,7 +113,7 @@ By default Corp Stats are only updated on demand. If you want to automatically r
|
||||
```python
|
||||
CELERYBEAT_SCHEDULE['update_all_corpstats'] = {
|
||||
'task': 'allianceauth.corputils.tasks.update_all_corpstats',
|
||||
'schedule': crontab(minute=0, hour="*/6"),
|
||||
'schedule': crontab(minute="0", hour="*/6"),
|
||||
}
|
||||
```
|
||||
|
||||
|