mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ce1939040 | ||
|
|
322131cd4f | ||
|
|
55e6e92da5 | ||
|
|
e5d29629a5 | ||
|
|
26e187e4c8 | ||
|
|
3480c4e0e8 | ||
|
|
1544f097e0 | ||
|
|
2477c31656 | ||
|
|
0dc631d69e | ||
|
|
2a9981cdb9 | ||
|
|
424246df26 | ||
|
|
563e2210ef | ||
|
|
02a1078005 | ||
|
|
30107de44e | ||
|
|
77a08cd218 | ||
|
|
e5a09027e5 | ||
|
|
52b6c5d341 | ||
|
|
8b895b76b5 | ||
|
|
babd71702f | ||
|
|
3ec3cbdff7 | ||
|
|
51611e1237 | ||
|
|
39519bab91 | ||
|
|
90dc6a4d4c | ||
|
|
53ffd7f885 | ||
|
|
efc7475228 | ||
|
|
380c41400b | ||
|
|
079c12a72e | ||
|
|
4f1ebedc44 | ||
|
|
66822107e3 | ||
|
|
7856cd5ce4 |
@@ -1,5 +1,5 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
host = https://app.transifex.com
|
||||
lang_map = zh-Hans: zh_Hans
|
||||
|
||||
[o:alliance-auth:p:alliance-auth:r:django-po]
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = zh-Hans:zh_Hans
|
||||
|
||||
[alliance-auth.django-po]
|
||||
file_filter = allianceauth/locale/<lang>/LC_MESSAGES/django.po
|
||||
minimum_perc = 0
|
||||
source_file = allianceauth/locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
10
.tx/transifex.yml
Normal file
10
.tx/transifex.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
filters:
|
||||
- filter_type: file
|
||||
file_format: PO
|
||||
source_file: allianceauth/locale/en/LC_MESSAGES/django.po
|
||||
source_language: en
|
||||
translation_files_expression: allianceauth/locale/<lang>/LC_MESSAGES/django.po
|
||||
|
||||
settings:
|
||||
language_mapping:
|
||||
zh-Hans: zh_Hans
|
||||
@@ -5,7 +5,7 @@ manage online service access.
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '3.6.1'
|
||||
__version__ = '3.8.0'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -65,7 +65,7 @@ class StateBackend(ModelBackend):
|
||||
# we've seen this character owner before. Re-attach to their old user account
|
||||
user = records[0].user
|
||||
if user.profile.main_character:
|
||||
if ownership.user.profile.main_character.character_id != token.character_id:
|
||||
if user.profile.main_character.character_id != token.character_id:
|
||||
## this is an alt, enforce main only due to trust issues in SSO.
|
||||
if request:
|
||||
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.")
|
||||
|
||||
0
allianceauth/authentication/core/__init__.py
Normal file
0
allianceauth/authentication/core/__init__.py
Normal file
48
allianceauth/authentication/core/celery_workers.py
Normal file
48
allianceauth/authentication/core/celery_workers.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""API for interacting with celery workers."""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from amqp.exceptions import ChannelError
|
||||
from celery import current_app
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def active_tasks_count() -> Optional[int]:
|
||||
"""Return count of currently active tasks
|
||||
or None if celery workers are not online.
|
||||
"""
|
||||
inspect = current_app.control.inspect()
|
||||
return _tasks_count(inspect.active())
|
||||
|
||||
|
||||
def _tasks_count(data: dict) -> Optional[int]:
|
||||
"""Return count of tasks in data from celery inspect API."""
|
||||
try:
|
||||
tasks = itertools.chain(*data.values())
|
||||
except AttributeError:
|
||||
return None
|
||||
return len(list(tasks))
|
||||
|
||||
|
||||
def queued_tasks_count() -> Optional[int]:
|
||||
"""Return count of queued tasks. Return None if there was an error."""
|
||||
try:
|
||||
with current_app.connection_or_acquire() as conn:
|
||||
result = conn.default_channel.queue_declare(
|
||||
queue=getattr(settings, "CELERY_DEFAULT_QUEUE", "celery"), passive=True
|
||||
)
|
||||
return result.message_count
|
||||
|
||||
except ChannelError:
|
||||
# Queue doesn't exist, probably empty
|
||||
return 0
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to get celery queue length")
|
||||
|
||||
return None
|
||||
@@ -4,13 +4,11 @@ import datetime as dt
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from .event_series import EventSeries
|
||||
from .helpers import ItemCounter
|
||||
|
||||
# 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")
|
||||
|
||||
|
||||
class _TaskCounts(NamedTuple):
|
||||
@@ -20,7 +18,6 @@ class _TaskCounts(NamedTuple):
|
||||
total: int
|
||||
earliest_task: Optional[dt.datetime]
|
||||
hours: int
|
||||
running: int
|
||||
|
||||
|
||||
def dashboard_results(hours: int) -> _TaskCounts:
|
||||
@@ -38,7 +35,6 @@ def dashboard_results(hours: int) -> _TaskCounts:
|
||||
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,
|
||||
@@ -46,5 +42,4 @@ 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,12 +1,9 @@
|
||||
"""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__)
|
||||
@@ -37,62 +34,6 @@ class _RedisStub:
|
||||
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()
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"""Signals for Task Statistics."""
|
||||
|
||||
from celery.signals import (
|
||||
task_failure, task_internal_error, task_postrun, task_prerun, task_retry,
|
||||
task_success, worker_ready,
|
||||
task_failure, task_internal_error, task_retry, task_success, worker_ready,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .counters import (
|
||||
failed_tasks, retried_tasks, running_tasks, succeeded_tasks,
|
||||
)
|
||||
from .counters import failed_tasks, retried_tasks, succeeded_tasks
|
||||
|
||||
|
||||
def reset_counters():
|
||||
@@ -17,7 +14,6 @@ def reset_counters():
|
||||
succeeded_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
running_tasks.reset()
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
@@ -55,15 +51,3 @@ 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()
|
||||
|
||||
@@ -4,11 +4,7 @@ from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.counters import (
|
||||
dashboard_results,
|
||||
succeeded_tasks,
|
||||
retried_tasks,
|
||||
failed_tasks,
|
||||
running_tasks,
|
||||
dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +28,6 @@ class TestDashboardResults(TestCase):
|
||||
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
failed_tasks.add()
|
||||
|
||||
running_tasks.reset(8)
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
@@ -41,14 +36,12 @@ 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
|
||||
@@ -57,4 +50,3 @@ class TestDashboardResults(TestCase):
|
||||
self.assertEqual(results.failed, 0)
|
||||
self.assertEqual(results.total, 0)
|
||||
self.assertIsNone(results.earliest_task)
|
||||
self.assertEqual(results.running, 0)
|
||||
|
||||
@@ -4,125 +4,11 @@ from unittest.mock import patch
|
||||
from redis import RedisError
|
||||
|
||||
from allianceauth.authentication.task_statistics.helpers import (
|
||||
ItemCounter, _RedisStub, get_redis_client_or_stub,
|
||||
_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):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
0
allianceauth/authentication/tests/core/__init__.py
Normal file
0
allianceauth/authentication/tests/core/__init__.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from amqp.exceptions import ChannelError
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.authentication.core.celery_workers import (
|
||||
active_tasks_count, queued_tasks_count,
|
||||
)
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.core.celery_workers"
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".current_app")
|
||||
class TestActiveTasksCount(TestCase):
|
||||
def test_should_return_correct_count_when_no_active_tasks(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue": []
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_should_return_correct_task_count_for_active_tasks(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue": [1, 2, 3]
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 3)
|
||||
|
||||
def test_should_return_correct_task_count_for_multiple_queues(
|
||||
self, mock_current_app
|
||||
):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue_1": [1, 2],
|
||||
"queue_2": [3, 4],
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 4)
|
||||
|
||||
def test_should_return_none_when_celery_not_available(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = None
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".current_app")
|
||||
class TestQueuedTasksCount(TestCase):
|
||||
def test_should_return_queue_length_when_queue_exists(self, mock_current_app):
|
||||
# given
|
||||
mock_conn = (
|
||||
mock_current_app.connection_or_acquire.return_value.__enter__.return_value
|
||||
)
|
||||
mock_conn.default_channel.queue_declare.return_value.message_count = 7
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 7)
|
||||
|
||||
def test_should_return_0_when_queue_does_not_exists(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.connection_or_acquire.side_effect = ChannelError
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_should_return_None_on_other_errors(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.connection_or_acquire.side_effect = RuntimeError
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
@@ -9,12 +9,8 @@ from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.templatetags.admin_status import (
|
||||
status_overview,
|
||||
_fetch_list_from_gitlab,
|
||||
_current_notifications,
|
||||
_current_version_summary,
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
_latests_versions
|
||||
_current_notifications, _current_version_summary, _fetch_list_from_gitlab,
|
||||
_fetch_notification_issues_from_gitlab, _latests_versions, status_overview,
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.templatetags'
|
||||
@@ -56,14 +52,10 @@ TEST_VERSION = '2.6.5'
|
||||
|
||||
class TestStatusOverviewTag(TestCase):
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
||||
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
||||
def test_status_overview(
|
||||
self,
|
||||
mock_current_notifications,
|
||||
mock_current_version_info,
|
||||
mock_fetch_celery_queue_length
|
||||
self, mock_current_notifications, mock_current_version_info
|
||||
):
|
||||
# given
|
||||
notifications = {
|
||||
@@ -82,7 +74,6 @@ class TestStatusOverviewTag(TestCase):
|
||||
'latest_beta_version': '2.4.4a1',
|
||||
}
|
||||
mock_current_version_info.return_value = version_info
|
||||
mock_fetch_celery_queue_length.return_value = 3
|
||||
# when
|
||||
result = status_overview()
|
||||
# then
|
||||
@@ -96,7 +87,6 @@ class TestStatusOverviewTag(TestCase):
|
||||
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
||||
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
||||
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
||||
self.assertEqual(result["task_queue_length"], 3)
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
|
||||
39
allianceauth/authentication/tests/test_views.py
Normal file
39
allianceauth/authentication/tests/test_views.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from allianceauth.authentication.views import task_counts
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.views"
|
||||
|
||||
|
||||
def jsonresponse_to_dict(response) -> dict:
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".queued_tasks_count")
|
||||
@patch(MODULE_PATH + ".active_tasks_count")
|
||||
class TestRunningTasksCount(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.factory = RequestFactory()
|
||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||
|
||||
def test_should_return_data(
|
||||
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||
):
|
||||
# given
|
||||
mock_active_tasks_count.return_value = 2
|
||||
mock_queued_tasks_count.return_value = 3
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = task_counts(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
|
||||
)
|
||||
@@ -38,4 +38,5 @@ urlpatterns = [
|
||||
name='token_refresh'
|
||||
),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('task-counts/', views.task_counts, name='task_counts'),
|
||||
]
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import logging
|
||||
|
||||
from django_registration.backends.activation.views import (
|
||||
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
||||
RegistrationView as BaseRegistrationView,
|
||||
)
|
||||
from django_registration.signals import user_registered
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login, authenticate
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import signing
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from esi.decorators import token_required
|
||||
from esi.models import Token
|
||||
|
||||
from django_registration.backends.activation.views import (
|
||||
RegistrationView as BaseRegistrationView,
|
||||
ActivationView as BaseActivationView,
|
||||
REGISTRATION_SALT
|
||||
)
|
||||
from django_registration.signals import user_registered
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
from .models import CharacterOwnership
|
||||
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||
from .forms import RegistrationForm
|
||||
from .models import CharacterOwnership
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
@@ -61,6 +61,7 @@ def dashboard(request):
|
||||
}
|
||||
return render(request, 'authentication/dashboard.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def token_management(request):
|
||||
tokens = request.user.token_set.all()
|
||||
@@ -70,6 +71,7 @@ def token_management(request):
|
||||
}
|
||||
return render(request, 'authentication/tokens.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def token_delete(request, token_id=None):
|
||||
try:
|
||||
@@ -83,6 +85,7 @@ def token_delete(request, token_id=None):
|
||||
messages.warning(request, "Token does not exist")
|
||||
return redirect('authentication:token_management')
|
||||
|
||||
|
||||
@login_required
|
||||
def token_refresh(request, token_id=None):
|
||||
try:
|
||||
@@ -127,7 +130,7 @@ def main_character_change(request, token):
|
||||
def add_character(request, token):
|
||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
||||
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name})))
|
||||
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
|
||||
else:
|
||||
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
|
||||
return redirect('authentication:dashboard')
|
||||
@@ -268,8 +271,11 @@ class ActivationView(BaseActivationView):
|
||||
|
||||
def validate_key(self, activation_key):
|
||||
try:
|
||||
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
|
||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
|
||||
dump = signing.loads(
|
||||
activation_key,
|
||||
salt=REGISTRATION_SALT,
|
||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400
|
||||
)
|
||||
return dump
|
||||
except signing.BadSignature:
|
||||
return None
|
||||
@@ -299,3 +305,12 @@ def activation_complete(request):
|
||||
def registration_closed(request):
|
||||
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
||||
return redirect('authentication:login')
|
||||
|
||||
|
||||
def task_counts(request) -> JsonResponse:
|
||||
"""Return task counts as JSON for an AJAX call."""
|
||||
data = {
|
||||
"tasks_running": active_tasks_count(),
|
||||
"tasks_queued": queued_tasks_count()
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import functools
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.functions import Lower
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -8,6 +12,39 @@ from .models import ReservedGroupName
|
||||
|
||||
|
||||
class GroupAdminForm(forms.ModelForm):
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
queryset=User.objects.order_by(Lower('username')),
|
||||
required=False,
|
||||
widget=FilteredSelectMultiple(verbose_name=_("Users"), is_stacked=False),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["users"].initial = self.instance.user_set.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
group: Group = super().save(commit=False)
|
||||
|
||||
if commit:
|
||||
group.save()
|
||||
|
||||
users = self.cleaned_data["users"]
|
||||
if group.pk:
|
||||
self._save_m2m_and_users(group, users)
|
||||
else:
|
||||
self.save_m2m = functools.partial(
|
||||
self._save_m2m_and_users, group=group, users=users
|
||||
)
|
||||
|
||||
return group
|
||||
|
||||
def _save_m2m_and_users(self, group, users):
|
||||
"""Save m2m relations incl. users."""
|
||||
group.user_set.set(users)
|
||||
self._save_m2m()
|
||||
|
||||
def clean_name(self):
|
||||
my_name = self.cleaned_data['name']
|
||||
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import Set
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -14,7 +13,7 @@ from allianceauth.notifications import notify
|
||||
class GroupRequest(models.Model):
|
||||
"""Request from a user for joining or leaving a group."""
|
||||
|
||||
leave_request = models.BooleanField(default=0)
|
||||
leave_request = models.BooleanField(default=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
|
||||
@@ -49,7 +48,7 @@ class RequestLog(models.Model):
|
||||
request_type = models.BooleanField(null=True)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
request_info = models.CharField(max_length=254)
|
||||
action = models.BooleanField(default=0)
|
||||
action = models.BooleanField(default=False)
|
||||
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if not auto_leave %}
|
||||
{% if not show_leave_tab %}
|
||||
<li>
|
||||
<a data-toggle="tab" href="#leave">
|
||||
{% translate "Leave Requests" %}
|
||||
@@ -102,7 +102,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not auto_leave %}
|
||||
{% if not show_leave_tab %}
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -6,22 +6,22 @@ from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||
from django.test import Client, RequestFactory, TestCase, override_settings
|
||||
|
||||
from allianceauth.authentication.models import CharacterOwnership, State
|
||||
from allianceauth.eveonline.models import (
|
||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
EveAllianceInfo, EveCharacter, EveCorporationInfo,
|
||||
)
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from . import get_admin_change_view_url
|
||||
from ..admin import HasLeaderFilter, GroupAdmin, Group
|
||||
from ..admin import Group, GroupAdmin, HasLeaderFilter
|
||||
from ..models import ReservedGroupName
|
||||
|
||||
from . import get_admin_change_view_url
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
|
||||
|
||||
from ..admin import IsAutoGroupFilter
|
||||
else:
|
||||
_has_auto_groups = False
|
||||
@@ -621,21 +621,16 @@ class TestGroupAdmin2(TestCase):
|
||||
response = self.client.post(
|
||||
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||
data={
|
||||
"name": f"{group.name}",
|
||||
"authgroup-TOTAL_FORMS": "1",
|
||||
"authgroup-INITIAL_FORMS": "1",
|
||||
"authgroup-MIN_NUM_FORMS": "0",
|
||||
"authgroup-MAX_NUM_FORMS": "1",
|
||||
"authgroup-0-description": "",
|
||||
"authgroup-0-states": f"{member_state.pk}",
|
||||
"name": group.name,
|
||||
"users": [user_member.pk, user_guest.pk],
|
||||
"authgroup-TOTAL_FORMS": 1,
|
||||
"authgroup-INITIAL_FORMS": 1,
|
||||
"authgroup-MIN_NUM_FORMS": 0,
|
||||
"authgroup-MAX_NUM_FORMS": 1,
|
||||
"authgroup-0-states": member_state.pk,
|
||||
"authgroup-0-internal": "on",
|
||||
"authgroup-0-hidden": "on",
|
||||
"authgroup-0-group": f"{group.pk}",
|
||||
"authgroup-__prefix__-description": "",
|
||||
"authgroup-__prefix__-internal": "on",
|
||||
"authgroup-__prefix__-hidden": "on",
|
||||
"authgroup-__prefix__-group": f"{group.pk}",
|
||||
"_save": "Save"
|
||||
"authgroup-0-group": group.pk,
|
||||
}
|
||||
)
|
||||
# then
|
||||
@@ -644,6 +639,85 @@ class TestGroupAdmin2(TestCase):
|
||||
self.assertIn(group, user_member.groups.all())
|
||||
self.assertNotIn(group, user_guest.groups.all())
|
||||
|
||||
def test_should_add_user_to_existing_group(self):
|
||||
# given
|
||||
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||
user_lex = AuthUtils.create_user("Lex Luthor")
|
||||
group = Group.objects.create(name="dummy")
|
||||
user_bruce.groups.add(group)
|
||||
self.client.force_login(self.superuser)
|
||||
# when
|
||||
response = self.client.post(
|
||||
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||
data={
|
||||
"name": group.name,
|
||||
"users": [user_bruce.pk, user_lex.pk],
|
||||
"authgroup-TOTAL_FORMS": 1,
|
||||
"authgroup-INITIAL_FORMS": 1,
|
||||
"authgroup-MIN_NUM_FORMS": 0,
|
||||
"authgroup-MAX_NUM_FORMS": 1,
|
||||
"authgroup-0-internal": "on",
|
||||
"authgroup-0-hidden": "on",
|
||||
"authgroup-0-group": group.pk,
|
||||
}
|
||||
)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||
self.assertIn(group, user_bruce.groups.all())
|
||||
self.assertIn(group, user_lex.groups.all())
|
||||
|
||||
def test_should_remove_user_from_existing_group(self):
|
||||
# given
|
||||
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||
user_lex = AuthUtils.create_user("Lex Luthor")
|
||||
group = Group.objects.create(name="dummy")
|
||||
user_bruce.groups.add(group)
|
||||
user_lex.groups.add(group)
|
||||
self.client.force_login(self.superuser)
|
||||
# when
|
||||
response = self.client.post(
|
||||
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||
data={
|
||||
"name": group.name,
|
||||
"users": user_bruce.pk,
|
||||
"authgroup-TOTAL_FORMS": 1,
|
||||
"authgroup-INITIAL_FORMS": 1,
|
||||
"authgroup-MIN_NUM_FORMS": 0,
|
||||
"authgroup-MAX_NUM_FORMS": 1,
|
||||
"authgroup-0-internal": "on",
|
||||
"authgroup-0-hidden": "on",
|
||||
"authgroup-0-group": group.pk,
|
||||
}
|
||||
)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||
self.assertIn(group, user_bruce.groups.all())
|
||||
self.assertNotIn(group, user_lex.groups.all())
|
||||
|
||||
def test_should_include_user_when_creating_group(self):
|
||||
# given
|
||||
user_bruce = AuthUtils.create_user("Bruce Wayne")
|
||||
self.client.force_login(self.superuser)
|
||||
# when
|
||||
response = self.client.post(
|
||||
"/admin/groupmanagement/group/add/",
|
||||
data={
|
||||
"name": "new group",
|
||||
"users": user_bruce.pk,
|
||||
"authgroup-TOTAL_FORMS": 1,
|
||||
"authgroup-INITIAL_FORMS": 0,
|
||||
"authgroup-MIN_NUM_FORMS": 0,
|
||||
"authgroup-MAX_NUM_FORMS": 1,
|
||||
}
|
||||
)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||
group = Group.objects.get(name="new group")
|
||||
self.assertIn(group, user_bruce.groups.all())
|
||||
|
||||
|
||||
class TestReservedGroupNameAdmin(TestCase):
|
||||
@classmethod
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from allianceauth.groupmanagement.models import Group, GroupRequest
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from .. import views
|
||||
@@ -16,6 +17,7 @@ class TestViews(TestCase):
|
||||
self.factory = RequestFactory()
|
||||
self.user = AuthUtils.create_user('Peter Parker')
|
||||
self.user_with_manage_permission = AuthUtils.create_user('Bruce Wayne')
|
||||
self.group = Group.objects.create(name="Example group")
|
||||
|
||||
# set permissions
|
||||
AuthUtils.add_permission_to_user_by_name(
|
||||
@@ -83,3 +85,19 @@ class TestViews(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertNotIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
|
||||
def test_should_not_hide_leave_requests_tab_when_there_are_open_requests(self):
|
||||
# given
|
||||
request = self.factory.get(reverse('groupmanagement:management'))
|
||||
request.user = self.user_with_manage_permission
|
||||
GroupRequest.objects.create(user=self.user, group=self.group, leave_request=True)
|
||||
|
||||
# when
|
||||
response = views.group_management(request)
|
||||
|
||||
# then
|
||||
content = response_content_to_str(response)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('<a data-toggle="tab" href="#leave">', content)
|
||||
self.assertIn('<div id="leave" class="tab-pane">', content)
|
||||
|
||||
@@ -2,13 +2,12 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db.models import Count
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allianceauth.notifications import notify
|
||||
@@ -16,7 +15,6 @@ from allianceauth.notifications import notify
|
||||
from .managers import GroupManager
|
||||
from .models import GroupRequest, RequestLog
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -45,10 +43,15 @@ def group_management(request):
|
||||
logger.debug("Providing user {} with {} acceptrequests and {} leaverequests.".format(
|
||||
request.user, len(acceptrequests), len(leaverequests)))
|
||||
|
||||
show_leave_tab = (
|
||||
getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False)
|
||||
and not GroupRequest.objects.filter(leave_request=True).exists()
|
||||
)
|
||||
|
||||
render_items = {
|
||||
'acceptrequests': acceptrequests,
|
||||
'leaverequests': leaverequests,
|
||||
'auto_leave': getattr(settings, 'GROUPMANAGEMENT_AUTO_LEAVE', False),
|
||||
'show_leave_tab': show_leave_tab,
|
||||
}
|
||||
|
||||
return render(request, 'groupmanagement/index.html', context=render_items)
|
||||
|
||||
Binary file not shown.
@@ -4,9 +4,9 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# Erik Kalkoken <erik.kalkoken@gmail.com>, 2020
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2021
|
||||
# Peter Pfeufer, 2022
|
||||
# Erik Kalkoken <erik.kalkoken@gmail.com>, 2023
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2023
|
||||
# Peter Pfeufer, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -14,8 +14,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: Peter Pfeufer, 2022\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Peter Pfeufer, 2023\n"
|
||||
"Language-Team: German (https://app.transifex.com/alliance-auth/teams/107430/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -34,7 +34,8 @@ msgstr "Google Analytics V4"
|
||||
#: allianceauth/authentication/decorators.py:37
|
||||
msgid "A main character is required to perform that action. Add one below."
|
||||
msgstr ""
|
||||
"Für diese Aktion wird ein Hauptcharacter benötigt. Bitte füge einen hinzu."
|
||||
"Zur Ausführung dieser Aktion ist ein Hauptcharakter erforderlich. Füge unten"
|
||||
" einen hinzu."
|
||||
|
||||
#: allianceauth/authentication/forms.py:12
|
||||
msgid "Email"
|
||||
@@ -131,7 +132,7 @@ msgstr "Hauptcharakter ändern"
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:125
|
||||
msgid "Group Memberships"
|
||||
msgstr "Gruppen"
|
||||
msgstr "Gruppenmitgliedschaften"
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:145
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:21
|
||||
@@ -206,7 +207,7 @@ msgstr ""
|
||||
#: allianceauth/authentication/views.py:83
|
||||
#, python-format
|
||||
msgid "Changed main character to %(char)s"
|
||||
msgstr "Haupcharakter geändert zu %(char)s"
|
||||
msgstr "Haupcharakter zu %(char)s geändert"
|
||||
|
||||
#: allianceauth/authentication/views.py:92
|
||||
#, python-format
|
||||
@@ -233,13 +234,12 @@ msgid ""
|
||||
"Sent confirmation email. Please follow the link to confirm your email "
|
||||
"address."
|
||||
msgstr ""
|
||||
"Bestätigungsmail gesendet. Bitte folge dem Link in der E-Mail zur "
|
||||
"Bestätigung."
|
||||
"Bestätigungs-E-Mail gesendet. Bitte folge dem Link, um Deine E-Mail-Adresse "
|
||||
"zu bestätigen."
|
||||
|
||||
#: allianceauth/authentication/views.py:257
|
||||
msgid "Confirmed your email address. Please login to continue."
|
||||
msgstr ""
|
||||
"Deine E-Mail Adresse wurde bestätigt. Bitte log Dich ein um fortzufahren."
|
||||
msgstr "Deine E-Mail Adresse wurde bestätigt. Bitte einloggen zum Fortfahren."
|
||||
|
||||
#: allianceauth/authentication/views.py:262
|
||||
msgid "Registration of new accounts is not allowed at this time."
|
||||
@@ -274,7 +274,7 @@ msgstr "Hauptcharaktere"
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:22
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:14
|
||||
msgid "Members"
|
||||
msgstr "Mitgliederzahl"
|
||||
msgstr "Mitglieder"
|
||||
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:35
|
||||
msgid "Unregistered"
|
||||
@@ -282,7 +282,7 @@ msgstr "Nicht registriert"
|
||||
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:38
|
||||
msgid "Last update:"
|
||||
msgstr "Letzes Update:"
|
||||
msgstr "Letzte Aktualisierung:"
|
||||
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:74
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:112
|
||||
@@ -382,11 +382,11 @@ msgstr "Charakter nicht registriert!"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
|
||||
msgid "This character is not associated with an auth account."
|
||||
msgstr "Dieser Charakter ist mit keinen Auth Konto verbunden."
|
||||
msgstr "Dieser Charakter ist keinem Auth Konto zugeordnet."
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
|
||||
msgid "Add it here"
|
||||
msgstr "Füge es hier hinzu"
|
||||
msgstr "Füge ihn hier hinzu"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
|
||||
msgid "before attempting to click fleet attendance links."
|
||||
@@ -394,7 +394,7 @@ msgstr "bevor Du versuchst auf FAT-Links zu klicken."
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:5
|
||||
msgid "Create Fatlink"
|
||||
msgstr "Erstelle FAT-Link"
|
||||
msgstr "FAT-Link erstellen"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:9
|
||||
#: allianceauth/optimer/templates/optimer/add.html:13
|
||||
@@ -409,20 +409,20 @@ msgstr "Fehlerhafte Anfrage!"
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:24
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:63
|
||||
msgid "Create fatlink"
|
||||
msgstr "Erstelle FAT-Link"
|
||||
msgstr "FAT-Link erstellen"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:3
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:4
|
||||
msgid "Fatlink view"
|
||||
msgstr "FAT-Link sehen"
|
||||
msgstr "FAT-Link ansehen"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:7
|
||||
msgid "Edit fatlink"
|
||||
msgstr "Editiere FAT-Link"
|
||||
msgstr "FAT-Link bearbeiten"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:11
|
||||
msgid "Delete fat"
|
||||
msgstr "Lösche FAT"
|
||||
msgstr "FAT löschen"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:17
|
||||
msgid "Registered characters"
|
||||
@@ -497,7 +497,7 @@ msgstr[1] "%(user)s hat diesen Monat %(links)s FAT-Links eingesammelt."
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:26
|
||||
msgid "Times used"
|
||||
msgstr "male genutzt"
|
||||
msgstr "Wie oft genutzt"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:37
|
||||
#, python-format
|
||||
@@ -570,7 +570,7 @@ msgstr "FAT-Link Statistik"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:20
|
||||
msgid "Ticker"
|
||||
msgstr "Ticker: "
|
||||
msgstr "Ticker"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:8
|
||||
msgid "Participation data"
|
||||
@@ -594,7 +594,7 @@ msgstr "Letzter FAT-Link"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:58
|
||||
msgid "View statistics"
|
||||
msgstr "Statistik"
|
||||
msgstr "Statistiken ansehen"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:95
|
||||
msgid "No created fatlinks on record."
|
||||
@@ -713,8 +713,8 @@ msgid ""
|
||||
"States listed here will have the ability to join this group provided they "
|
||||
"have the proper permissions.<br>"
|
||||
msgstr ""
|
||||
"Hier gelistete Ränge können dieser Gruppe beitreten, vorausgesetzt sie haben"
|
||||
" die entsprechenden Berechtigungen.<br>"
|
||||
"Die hier aufgeführten Status können dieser Gruppe beitreten, sofern sie über"
|
||||
" die entsprechenden Berechtigungen verfügen.<br>"
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:171
|
||||
msgid ""
|
||||
@@ -814,7 +814,7 @@ msgstr "Gruppenmitglieder"
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:113
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:21
|
||||
msgid "Organization"
|
||||
msgstr "Organization"
|
||||
msgstr "Organisation"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:49
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:75
|
||||
@@ -933,18 +933,18 @@ msgstr "Gruppenverwaltung"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:24
|
||||
msgid "Join Requests"
|
||||
msgstr "Beitrittsgesuche"
|
||||
msgstr "Beitrittsanfragen"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:35
|
||||
msgid "Leave Requests"
|
||||
msgstr "Austrittsgesuche"
|
||||
msgstr "Austrittsanfragen"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:57
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:114
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:18
|
||||
#: allianceauth/services/modules/openfire/forms.py:6
|
||||
msgid "Group"
|
||||
msgstr "Gruppen"
|
||||
msgstr "Gruppe"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:88
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:145
|
||||
@@ -968,7 +968,7 @@ msgstr "Keine Gruppenaustrittsanfragen"
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:8
|
||||
#: allianceauth/templates/allianceauth/top-menu.html:8
|
||||
msgid "Toggle navigation"
|
||||
msgstr "Toggle Navigation"
|
||||
msgstr "Navigation umschalten"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:19
|
||||
msgid "Group Requests"
|
||||
@@ -994,7 +994,7 @@ msgstr "Gruppe existiert nicht"
|
||||
#: allianceauth/groupmanagement/views.py:195
|
||||
#, python-format
|
||||
msgid "Accepted application from %(mainchar)s to %(group)s."
|
||||
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s zugestimmt."
|
||||
msgstr "Beitrittsanfrage von %(mainchar)s zur Gruppe %(group)s akzeptiert."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:201
|
||||
#: allianceauth/groupmanagement/views.py:232
|
||||
@@ -1003,18 +1003,18 @@ msgid ""
|
||||
"An unhandled error occurred while processing the application from "
|
||||
"%(mainchar)s to %(group)s."
|
||||
msgstr ""
|
||||
"Bei der Bearbeitung des Beitrittsgesuchs von %(mainchar)s zur Gruppe "
|
||||
"Bei der Bearbeitung des Beitrittsanfrage von %(mainchar)s zur Gruppe "
|
||||
"%(group)s ist ein unbehandelter Fehler aufgetreten."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:226
|
||||
#, python-format
|
||||
msgid "Rejected application from %(mainchar)s to %(group)s."
|
||||
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s abgelehnt."
|
||||
msgstr "Beitrittsanfrage von %(mainchar)s zur Gruppe %(group)s abgelehnt."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:261
|
||||
#, python-format
|
||||
msgid "Accepted application from %(mainchar)s to leave %(group)s."
|
||||
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s akzeptiert."
|
||||
msgstr "Austrittsanfrage von %(mainchar)s für Gruppe %(group)s akzeptiert."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:266
|
||||
#: allianceauth/groupmanagement/views.py:298
|
||||
@@ -1023,13 +1023,13 @@ msgid ""
|
||||
"An unhandled error occurred while processing the application from "
|
||||
"%(mainchar)s to leave %(group)s."
|
||||
msgstr ""
|
||||
"Bei der Bearbeitung des Austrittsgesuchs von %(mainchar)s für Gruppe "
|
||||
"Bei der Bearbeitung des Austrittsanfrage von %(mainchar)s für Gruppe "
|
||||
"%(group)s ist ein unbehandelter Fehler aufgetreten."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:292
|
||||
#, python-format
|
||||
msgid "Rejected application from %(mainchar)s to leave %(group)s."
|
||||
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s abgelehnt."
|
||||
msgstr "Austrittsanfrage von %(mainchar)s für Gruppe %(group)s abgelehnt."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:336
|
||||
#: allianceauth/groupmanagement/views.py:346
|
||||
@@ -1042,7 +1042,7 @@ msgstr "Du bist bereits Mitglied dieser Gruppe."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:358
|
||||
msgid "You already have a pending application for that group."
|
||||
msgstr "Du hast Dich bereits für diese Gruppe beworben."
|
||||
msgstr "Du hast bereits für diese Gruppe angefragt."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:367
|
||||
#, python-format
|
||||
@@ -1059,12 +1059,12 @@ msgstr "Du bist kein Mitglied dieser Gruppe"
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:393
|
||||
msgid "You already have a pending leave request for that group."
|
||||
msgstr "Du hast bereits ein ausstehendes Austrittsgesuch für diese Gruppe."
|
||||
msgstr "Du hast bereits eine ausstehendes Austrittsanfrage für diese Gruppe."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:409
|
||||
#, python-format
|
||||
msgid "Applied to leave group %(group)s."
|
||||
msgstr "Austrittsgesuch für Gruppe %(group)s gesendet."
|
||||
msgstr "Austrittsanfrage für Gruppe %(group)s gesendet."
|
||||
|
||||
#: allianceauth/hrapplications/auth_hooks.py:14
|
||||
msgid "Applications"
|
||||
@@ -1086,11 +1086,11 @@ msgstr "Wähle eine Corporation"
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:10
|
||||
msgid "Available Corps"
|
||||
msgstr "Zur Auswahl stehende Corporations"
|
||||
msgstr "Verfügbare Corporationen"
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:22
|
||||
msgid "No corps are accepting applications at this time."
|
||||
msgstr "Zur Zeit nimmt keine Corp Bewerbungen entgegen."
|
||||
msgstr "Zur Zeit nimmt keine Corp Bewerbungen an."
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/create.html:4
|
||||
#: allianceauth/hrapplications/templates/hrapplications/create.html:7
|
||||
@@ -1186,7 +1186,7 @@ msgstr "Keine angesehenen Bewerbungen"
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:62
|
||||
#: allianceauth/hrapplications/templates/hrapplications/view.html:134
|
||||
msgid "Close"
|
||||
msgstr "Geschlossen"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:177
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:63
|
||||
@@ -1200,7 +1200,7 @@ msgstr "Suche"
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:11
|
||||
msgid "Application Search Results"
|
||||
msgstr "Bewerbungen Suchergebnisse"
|
||||
msgstr "Ergebnisse der Bewerbungssuche"
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:22
|
||||
msgid "Application ID"
|
||||
@@ -1342,12 +1342,12 @@ msgstr "Operationsart"
|
||||
#: allianceauth/optimer/form.py:17
|
||||
#: allianceauth/srp/templates/srp/management.html:38
|
||||
msgid "Fleet Commander"
|
||||
msgstr "Flottenkommandeur"
|
||||
msgstr "Flottenkommandant"
|
||||
|
||||
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
|
||||
#: allianceauth/srp/templates/srp/data.html:91
|
||||
msgid "Additional Info"
|
||||
msgstr "Zusätzliche Info"
|
||||
msgstr "Zusätzliche Informationen"
|
||||
|
||||
#: allianceauth/optimer/form.py:23
|
||||
msgid "(Optional) Describe the operation with a couple of short words."
|
||||
@@ -1360,7 +1360,7 @@ msgstr "Operation erstellen"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/fleetoptable.html:12
|
||||
msgid "Form Up System"
|
||||
msgstr "Form Up System"
|
||||
msgstr "Startsystem"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/fleetoptable.html:14
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:36
|
||||
@@ -1384,20 +1384,20 @@ msgstr "Flottenoperationen Zeiten"
|
||||
#: allianceauth/optimer/templates/optimer/management.html:20
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:22
|
||||
msgid "Current Eve Time:"
|
||||
msgstr "Momentane Eve Zeit"
|
||||
msgstr "Aktuelle Eve Zeit"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:26
|
||||
msgid "Next Fleet Operations"
|
||||
msgstr "Anstehende Flottenoperationen"
|
||||
msgstr "Anstehende Flotten"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:30
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:362
|
||||
msgid "No upcoming timers."
|
||||
msgstr "Keine kommenden Timer."
|
||||
msgstr "Keine bevorstehenden Timer."
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:33
|
||||
msgid "Past Fleet Operations"
|
||||
msgstr "Vergangene Flottenoperationen"
|
||||
msgstr "Vergangene Flotten"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:37
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:535
|
||||
@@ -1408,7 +1408,7 @@ msgstr "Keine vergangenen Timer."
|
||||
#: allianceauth/optimer/templates/optimer/update.html:15
|
||||
#: allianceauth/optimer/templates/optimer/update.html:27
|
||||
msgid "Update Fleet Operation"
|
||||
msgstr "Aktualisiere Flottenoperationen"
|
||||
msgstr "Aktualisiere Flottenoperation"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/update.html:21
|
||||
msgid "Fleet Operation Does Not Exist"
|
||||
@@ -1432,7 +1432,7 @@ msgstr "Änderungen für Operation timer %(opname)s gespeichert."
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:4
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:8
|
||||
msgid "Permissions Audit"
|
||||
msgstr "Berechtigungsübersicht"
|
||||
msgstr "Berechtigungsprüfung"
|
||||
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:20
|
||||
msgid "User / Character"
|
||||
@@ -1494,11 +1494,11 @@ msgstr "Dienste"
|
||||
|
||||
#: allianceauth/services/forms.py:6
|
||||
msgid "Name of Fleet:"
|
||||
msgstr "SRP Flotte erstellen:"
|
||||
msgstr "Name der Flotte:"
|
||||
|
||||
#: allianceauth/services/forms.py:7
|
||||
msgid "Fleet Commander:"
|
||||
msgstr "Flottenkommandeur:"
|
||||
msgstr "Flottenkommandant:"
|
||||
|
||||
#: allianceauth/services/forms.py:8
|
||||
msgid "Fleet Comms:"
|
||||
@@ -1514,11 +1514,11 @@ msgstr "Schiffspriorität:"
|
||||
|
||||
#: allianceauth/services/forms.py:11
|
||||
msgid "Formup Location:"
|
||||
msgstr "Formup Location:"
|
||||
msgstr "Startsystem:"
|
||||
|
||||
#: allianceauth/services/forms.py:12
|
||||
msgid "Formup Time:"
|
||||
msgstr "Formup Zeit:"
|
||||
msgstr "Startzeit:"
|
||||
|
||||
#: allianceauth/services/forms.py:13
|
||||
msgid "Expected Duration:"
|
||||
@@ -1530,7 +1530,7 @@ msgstr "Grund:"
|
||||
|
||||
#: allianceauth/services/forms.py:15
|
||||
msgid "Reimbursable?*"
|
||||
msgstr "Erstattungsfähig?"
|
||||
msgstr "Erstattungsfähig?*"
|
||||
|
||||
#: allianceauth/services/forms.py:15 allianceauth/services/forms.py:16
|
||||
msgid "Yes"
|
||||
@@ -1542,7 +1542,7 @@ msgstr "Nein"
|
||||
|
||||
#: allianceauth/services/forms.py:16
|
||||
msgid "Important?*"
|
||||
msgstr "Wichtig?"
|
||||
msgstr "Wichtig?*"
|
||||
|
||||
#: allianceauth/services/forms.py:21 allianceauth/services/forms.py:31
|
||||
msgid "Password"
|
||||
@@ -1550,7 +1550,7 @@ msgstr "Passwort"
|
||||
|
||||
#: allianceauth/services/forms.py:26 allianceauth/services/forms.py:36
|
||||
msgid "Password must be at least 8 characters long."
|
||||
msgstr "Passwort muss mindestens 8 Zeichen lang sein"
|
||||
msgstr "Das Passwort muss mindestens 8 Zeichen lang sein"
|
||||
|
||||
#: allianceauth/services/modules/discord/models.py:187
|
||||
msgid "Discord Account Disabled"
|
||||
@@ -1591,7 +1591,7 @@ msgstr "Discord Konto deaktiviert."
|
||||
#: allianceauth/services/modules/discord/views.py:36
|
||||
#: allianceauth/services/modules/discord/views.py:59
|
||||
msgid "An error occurred while processing your Discord account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Discord Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines Discord Kontos."
|
||||
|
||||
#: allianceauth/services/modules/discord/views.py:102
|
||||
msgid "Your Discord account has been successfully activated."
|
||||
@@ -1607,7 +1607,7 @@ msgstr ""
|
||||
|
||||
#: allianceauth/services/modules/discourse/views.py:29
|
||||
msgid "You are not authorized to access Discourse."
|
||||
msgstr "Du bist nicht autorisiert auf Discorse zuzugreifen."
|
||||
msgstr "Du bist nicht autorisiert auf Discourse zuzugreifen."
|
||||
|
||||
#: allianceauth/services/modules/discourse/views.py:34
|
||||
msgid "You must have a main character set to access Discourse."
|
||||
@@ -1619,14 +1619,14 @@ msgid ""
|
||||
"No SSO payload or signature. Please contact support if this problem "
|
||||
"persists."
|
||||
msgstr ""
|
||||
"Keine SSO-Nutzdaten oder Signaturen. Bitte wenden Sie sich an den Support, "
|
||||
"wenn das Problem weiterhin besteht."
|
||||
"Keine SSO-Nutzdaten oder Signaturen. Bitte wende Dich an den Support, wenn "
|
||||
"das Problem weiterhin besteht."
|
||||
|
||||
#: allianceauth/services/modules/discourse/views.py:54
|
||||
#: allianceauth/services/modules/discourse/views.py:62
|
||||
msgid "Invalid payload. Please contact support if this problem persists."
|
||||
msgstr ""
|
||||
"Ungültige Nutzdaten. Bitte wenden Sie sich an den Support, wenn das Problem "
|
||||
"Ungültige Nutzdaten. Bitte wenden Dich an den Support, wenn das Problem "
|
||||
"weiterhin besteht."
|
||||
|
||||
#: allianceauth/services/modules/ips4/views.py:31
|
||||
@@ -1638,7 +1638,7 @@ msgstr "IP4Suite Konto aktiviert."
|
||||
#: allianceauth/services/modules/ips4/views.py:81
|
||||
#: allianceauth/services/modules/ips4/views.py:101
|
||||
msgid "An error occurred while processing your IPSuite4 account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines IPSuite4 Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines IPSuite4 Kontos."
|
||||
|
||||
#: allianceauth/services/modules/ips4/views.py:52
|
||||
msgid "Reset IPSuite4 password."
|
||||
@@ -1660,7 +1660,7 @@ msgstr "Jabber"
|
||||
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:5
|
||||
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:10
|
||||
msgid "Jabber Broadcast"
|
||||
msgstr "Jabber Übertragung"
|
||||
msgstr "Jabber Ankündigung"
|
||||
|
||||
#: allianceauth/services/modules/openfire/auth_hooks.py:94
|
||||
msgid "Fleet Broadcast Formatter"
|
||||
@@ -1672,11 +1672,11 @@ msgstr "Nachricht"
|
||||
|
||||
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:16
|
||||
msgid "Broadcast Sent!!"
|
||||
msgstr "Übertragung gesendet!!"
|
||||
msgstr "Ankündigung gesendet!!"
|
||||
|
||||
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:22
|
||||
msgid "Broadcast"
|
||||
msgstr "Übertragungen"
|
||||
msgstr "Ankündigung"
|
||||
|
||||
#: allianceauth/services/modules/openfire/views.py:35
|
||||
msgid "Activated jabber account."
|
||||
@@ -1687,7 +1687,7 @@ msgstr "Jabber Konto aktiviert."
|
||||
#: allianceauth/services/modules/openfire/views.py:76
|
||||
#: allianceauth/services/modules/openfire/views.py:147
|
||||
msgid "An error occurred while processing your jabber account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Jabber Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines Jabber Kontos."
|
||||
|
||||
#: allianceauth/services/modules/openfire/views.py:69
|
||||
msgid "Reset jabber password."
|
||||
@@ -1696,7 +1696,7 @@ msgstr "Jabber Passwort zurücksetzen."
|
||||
#: allianceauth/services/modules/openfire/views.py:115
|
||||
#, python-format
|
||||
msgid "Sent jabber broadcast to %s"
|
||||
msgstr "Sende Jabber Durchsage an %s"
|
||||
msgstr "Sende Jabber Ankündigung an %s"
|
||||
|
||||
#: allianceauth/services/modules/openfire/views.py:144
|
||||
msgid "Set jabber password."
|
||||
@@ -1711,7 +1711,7 @@ msgstr "Forum Konto aktiviert."
|
||||
#: allianceauth/services/modules/phpbb3/views.py:78
|
||||
#: allianceauth/services/modules/phpbb3/views.py:101
|
||||
msgid "An error occurred while processing your forum account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Forum Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines Forum Kontos."
|
||||
|
||||
#: allianceauth/services/modules/phpbb3/views.py:53
|
||||
msgid "Deactivated forum account."
|
||||
@@ -1734,7 +1734,7 @@ msgstr "SMF Konto aktiviert."
|
||||
#: allianceauth/services/modules/smf/views.py:102
|
||||
#: allianceauth/services/modules/smf/views.py:124
|
||||
msgid "An error occurred while processing your SMF account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines SMF Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines SMF Kontos."
|
||||
|
||||
#: allianceauth/services/modules/smf/views.py:78
|
||||
msgid "Deactivated SMF account."
|
||||
@@ -1751,7 +1751,7 @@ msgstr "Setze SMF Passwort."
|
||||
#: allianceauth/services/modules/teamspeak3/forms.py:14
|
||||
#, python-format
|
||||
msgid "Unable to locate user %s on server"
|
||||
msgstr "Kann den Benutzer %s auf dem Server nicht finden"
|
||||
msgstr "Der Benutzer %s konnte auf dem Server nicht gefunden werden"
|
||||
|
||||
#: allianceauth/services/modules/teamspeak3/templates/admin/teamspeak3/authts/change_list.html:8
|
||||
msgid "Update TS3 groups"
|
||||
@@ -1783,7 +1783,8 @@ msgstr "TeamSpeak3 Konto aktiviert."
|
||||
#: allianceauth/services/modules/teamspeak3/views.py:74
|
||||
#: allianceauth/services/modules/teamspeak3/views.py:100
|
||||
msgid "An error occurred while processing your TeamSpeak3 account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines TeamSpeak3 Kontos."
|
||||
msgstr ""
|
||||
"Es gab einen Fehler während der Verarbeitung Deines TeamSpeak3 Kontos."
|
||||
|
||||
#: allianceauth/services/modules/teamspeak3/views.py:71
|
||||
msgid "Deactivated TeamSpeak3 account."
|
||||
@@ -1802,7 +1803,7 @@ msgstr "XenForo Konto aktiviert."
|
||||
#: allianceauth/services/modules/xenforo/views.py:73
|
||||
#: allianceauth/services/modules/xenforo/views.py:94
|
||||
msgid "An error occurred while processing your XenForo account."
|
||||
msgstr "Es gab einen Fehler bei der Verarbeitung Deines XenForo Kontos."
|
||||
msgstr "Es gab einen Fehler während der Verarbeitung Deines XenForo Kontos."
|
||||
|
||||
#: allianceauth/services/modules/xenforo/views.py:50
|
||||
msgid "Deactivated XenForo account."
|
||||
@@ -1832,7 +1833,7 @@ msgstr "Formatieren"
|
||||
#: allianceauth/services/templates/services/service_confirm_delete.html:12
|
||||
#, python-format
|
||||
msgid "Delete %(service_name)s Account?"
|
||||
msgstr "Konto %(service_name)s löschen?"
|
||||
msgstr " %(service_name)s Konto löschen?"
|
||||
|
||||
#: allianceauth/services/templates/services/service_confirm_delete.html:20
|
||||
#, python-format
|
||||
@@ -1856,7 +1857,7 @@ msgstr "%(service_name)s Passwort ändern"
|
||||
#: allianceauth/services/templates/services/service_password.html:9
|
||||
#, python-format
|
||||
msgid "Set %(service_name)s Password"
|
||||
msgstr "%(service_name)s Passwort"
|
||||
msgstr "%(service_name)s Passwort setzen"
|
||||
|
||||
#: allianceauth/services/templates/services/service_password.html:17
|
||||
msgid "Set Password"
|
||||
@@ -1927,7 +1928,7 @@ msgstr "SRP Flotten Daten"
|
||||
|
||||
#: allianceauth/srp/templates/srp/data.html:50
|
||||
msgid "SRP Fleet Data"
|
||||
msgstr "SRP-Flotte Daten"
|
||||
msgstr "SRP Flotte Daten"
|
||||
|
||||
#: allianceauth/srp/templates/srp/data.html:55
|
||||
msgid "Mark Incomplete"
|
||||
@@ -2005,7 +2006,7 @@ msgstr "Füge SRP Flotte hinzu"
|
||||
|
||||
#: allianceauth/srp/templates/srp/management.html:39
|
||||
msgid "Fleet AAR"
|
||||
msgstr "Flotten AAR"
|
||||
msgstr "Flottenbericht"
|
||||
|
||||
#: allianceauth/srp/templates/srp/management.html:40
|
||||
msgid "Fleet SRP Code"
|
||||
@@ -2033,7 +2034,7 @@ msgstr "Deaktiviert"
|
||||
|
||||
#: allianceauth/srp/templates/srp/management.html:83
|
||||
msgid "Completed"
|
||||
msgstr "Fertig"
|
||||
msgstr "Abgeschlossen"
|
||||
|
||||
#: allianceauth/srp/templates/srp/management.html:101
|
||||
msgid "Are you sure you want to delete this SRP code and its contents?"
|
||||
@@ -2086,7 +2087,7 @@ msgstr "SRP Link für %(fleetname)s aktiviert."
|
||||
#: allianceauth/srp/views.py:140
|
||||
#, python-format
|
||||
msgid "Marked SRP fleet %(fleetname)s as completed."
|
||||
msgstr "SRP Flotte %(fleetname)s als vollständig markiert."
|
||||
msgstr "SRP Flotte %(fleetname)s als abgeschlossen markiert."
|
||||
|
||||
#: allianceauth/srp/views.py:153
|
||||
#, python-format
|
||||
@@ -2204,7 +2205,7 @@ msgstr "Testversion verfügbar"
|
||||
|
||||
#: allianceauth/templates/allianceauth/admin-status/overview.html:78
|
||||
msgid "Task Queue"
|
||||
msgstr "Warteschlange"
|
||||
msgstr "Task-Warteschlange"
|
||||
|
||||
#: allianceauth/templates/allianceauth/admin-status/overview.html:81
|
||||
#, python-format
|
||||
@@ -2249,7 +2250,7 @@ msgstr "Ausloggen"
|
||||
|
||||
#: allianceauth/timerboard/form.py:53
|
||||
msgid "Other"
|
||||
msgstr "anderes"
|
||||
msgstr "Anderes"
|
||||
|
||||
#: allianceauth/timerboard/form.py:54
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:62
|
||||
@@ -2353,7 +2354,7 @@ msgstr "Timer löschen"
|
||||
#: allianceauth/timerboard/templates/timerboard/timer_confirm_delete.html:19
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete timer \"%(object)s\"?"
|
||||
msgstr "Bist Du sicher das Du Timer \"%(object)s\" löschen möchtest?"
|
||||
msgstr "Bist Du sicher das Du Timer „%(object)s“ löschen möchtest?"
|
||||
|
||||
#: allianceauth/timerboard/templates/timerboard/timer_create_form.html:5
|
||||
#: allianceauth/timerboard/templates/timerboard/timer_create_form.html:13
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"POT-Creation-Date: 2023-11-08 23:55+1000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -26,7 +26,7 @@ msgstr ""
|
||||
msgid "Google Analytics V4"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/decorators.py:37
|
||||
#: allianceauth/authentication/decorators.py:49
|
||||
msgid "A main character is required to perform that action. Add one below."
|
||||
msgstr ""
|
||||
|
||||
@@ -39,63 +39,68 @@ msgstr ""
|
||||
msgid "You are not allowed to add or remove these restricted groups: %s"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:80
|
||||
#: allianceauth/authentication/models.py:71
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:81
|
||||
#: allianceauth/authentication/models.py:72
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:82
|
||||
#: allianceauth/authentication/models.py:73
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:83
|
||||
#: allianceauth/authentication/models.py:74
|
||||
msgid "Chinese Simplified"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:84
|
||||
#: allianceauth/authentication/models.py:75
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:85
|
||||
#: allianceauth/authentication/models.py:76
|
||||
msgid "Korean"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:86
|
||||
#: allianceauth/authentication/models.py:77
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:87
|
||||
#: allianceauth/authentication/models.py:78
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:88
|
||||
#: allianceauth/authentication/models.py:79
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:91
|
||||
msgid "Language"
|
||||
#: allianceauth/authentication/models.py:80
|
||||
msgid "Ukrainian"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:96
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:101
|
||||
#: allianceauth/templates/allianceauth/night-toggle.html:6
|
||||
msgid "Night Mode"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:110
|
||||
#: allianceauth/authentication/models.py:115
|
||||
#, python-format
|
||||
msgid "State changed to: %s"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:111
|
||||
#: allianceauth/authentication/models.py:116
|
||||
#, python-format
|
||||
msgid "Your user's state is now: %(state)s"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:4
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:7
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:4
|
||||
#: allianceauth/templates/allianceauth/side-menu.html:10
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
@@ -151,8 +156,49 @@ msgstr ""
|
||||
msgid "Alliance"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:7
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62
|
||||
msgid "Token Management"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:12
|
||||
msgid "Scopes"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:13
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:28
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:127
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:27
|
||||
#: allianceauth/hrapplications/templates/hrapplications/view.html:73
|
||||
#: allianceauth/srp/templates/srp/data.html:101
|
||||
#: allianceauth/srp/templates/srp/management.html:44
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:14
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:74
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:112
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:156
|
||||
#: allianceauth/corputils/templates/corputils/search.html:13
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:22
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:26
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:30
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:55
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
|
||||
msgid "Character"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:28
|
||||
msgid ""
|
||||
"This page is a best attempt, but backups or database logs can still contain "
|
||||
"your tokens. Always revoke tokens on https://community.eveonline.com/support/"
|
||||
"third-party-applications/ where possible."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/public/login.html:6
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:58
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:69
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
@@ -184,47 +230,47 @@ msgstr ""
|
||||
msgid "Invalid or expired activation link."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:77
|
||||
#: allianceauth/authentication/views.py:118
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Cannot change main character to %(char)s: character owned by a different "
|
||||
"account."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:83
|
||||
#: allianceauth/authentication/views.py:124
|
||||
#, python-format
|
||||
msgid "Changed main character to %(char)s"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:92
|
||||
#: allianceauth/authentication/views.py:133
|
||||
#, python-format
|
||||
msgid "Added %(name)s to your account."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:94
|
||||
#: allianceauth/authentication/views.py:135
|
||||
#, python-format
|
||||
msgid "Failed to add %(name)s to your account: they already have an account."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:133
|
||||
#: allianceauth/authentication/views.py:174
|
||||
msgid "Unable to authenticate as the selected character."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:197
|
||||
#: allianceauth/authentication/views.py:238
|
||||
msgid "Registration token has expired."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:252
|
||||
#: allianceauth/authentication/views.py:296
|
||||
msgid ""
|
||||
"Sent confirmation email. Please follow the link to confirm your email "
|
||||
"address."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:257
|
||||
#: allianceauth/authentication/views.py:301
|
||||
msgid "Confirmed your email address. Please login to continue."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/views.py:262
|
||||
#: allianceauth/authentication/views.py:306
|
||||
msgid "Registration of new accounts is not allowed at this time."
|
||||
msgstr ""
|
||||
|
||||
@@ -267,19 +313,6 @@ msgstr ""
|
||||
msgid "Last update:"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:74
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:112
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:156
|
||||
#: allianceauth/corputils/templates/corputils/search.html:13
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:22
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:26
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:30
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:55
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
|
||||
msgid "Character"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/corputils/templates/corputils/corpstats.html:75
|
||||
#: allianceauth/corputils/templates/corputils/search.html:14
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
|
||||
@@ -611,36 +644,41 @@ msgstr ""
|
||||
msgid "Group Management"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/forms.py:15
|
||||
#: allianceauth/groupmanagement/forms.py:18
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/forms.py:52
|
||||
msgid "This name has been reserved and can not be used for groups."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/forms.py:25
|
||||
#: allianceauth/groupmanagement/forms.py:62
|
||||
msgid "(auto)"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/forms.py:34
|
||||
#: allianceauth/groupmanagement/forms.py:71
|
||||
msgid "There already exists a group with that name."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:105
|
||||
#: allianceauth/groupmanagement/models.py:104
|
||||
msgid ""
|
||||
"Internal group, users cannot see, join or request to join this group."
|
||||
"<br>Used for groups such as Members, Corp_*, Alliance_* etc.<br><b>Overrides "
|
||||
"Hidden and Open options when selected.</b>"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:113
|
||||
#: allianceauth/groupmanagement/models.py:112
|
||||
msgid "Group is hidden from users but can still join with the correct link."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:119
|
||||
#: allianceauth/groupmanagement/models.py:118
|
||||
msgid ""
|
||||
"Group is open and users will be automatically added upon request.<br>If the "
|
||||
"group is not open users will need their request manually approved."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:126
|
||||
#: allianceauth/groupmanagement/models.py:125
|
||||
msgid ""
|
||||
"Group is public. Any registered user is able to join this group, with "
|
||||
"visibility based on the other options set for this group.<br>Auth will not "
|
||||
@@ -648,65 +686,65 @@ msgid ""
|
||||
"authenticated."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:135
|
||||
#: allianceauth/groupmanagement/models.py:134
|
||||
msgid ""
|
||||
"Group is restricted. This means that adding or removing users for this group "
|
||||
"requires a superuser admin."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:144
|
||||
#: allianceauth/groupmanagement/models.py:143
|
||||
msgid ""
|
||||
"Group leaders can process requests for this group. Use the <code>auth."
|
||||
"group_management</code> permission to allow a user to manage all groups.<br>"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:154
|
||||
#: allianceauth/groupmanagement/models.py:153
|
||||
msgid ""
|
||||
"Members of leader groups can process requests for this group. Use the "
|
||||
"<code>auth.group_management</code> permission to allow a user to manage all "
|
||||
"groups.<br>"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:163
|
||||
#: allianceauth/groupmanagement/models.py:162
|
||||
msgid ""
|
||||
"States listed here will have the ability to join this group provided they "
|
||||
"have the proper permissions.<br>"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:171
|
||||
#: allianceauth/groupmanagement/models.py:170
|
||||
msgid ""
|
||||
"Short description <i>(max. 512 characters)</i> of the group shown to users."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:178
|
||||
#: allianceauth/groupmanagement/models.py:177
|
||||
msgid "Can request non-public groups"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:209
|
||||
#: allianceauth/groupmanagement/models.py:208
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:212
|
||||
#: allianceauth/groupmanagement/models.py:211
|
||||
msgid "Name that can not be used for groups."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:215
|
||||
#: allianceauth/groupmanagement/models.py:214
|
||||
msgid "reason"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:215
|
||||
#: allianceauth/groupmanagement/models.py:214
|
||||
msgid "Reason why this name is reserved."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:218
|
||||
#: allianceauth/groupmanagement/models.py:217
|
||||
msgid "created by"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:223
|
||||
#: allianceauth/groupmanagement/models.py:222
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:223
|
||||
#: allianceauth/groupmanagement/models.py:222
|
||||
msgid "Date when this entry was created"
|
||||
msgstr ""
|
||||
|
||||
@@ -933,86 +971,86 @@ msgstr ""
|
||||
msgid "Group Membership"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:163
|
||||
#: allianceauth/groupmanagement/views.py:166
|
||||
#, python-format
|
||||
msgid "Removed user %(user)s from group %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:165
|
||||
#: allianceauth/groupmanagement/views.py:168
|
||||
msgid "User does not exist in that group"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:168
|
||||
#: allianceauth/groupmanagement/views.py:171
|
||||
msgid "Group does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:195
|
||||
#: allianceauth/groupmanagement/views.py:198
|
||||
#, python-format
|
||||
msgid "Accepted application from %(mainchar)s to %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:201
|
||||
#: allianceauth/groupmanagement/views.py:232
|
||||
#: allianceauth/groupmanagement/views.py:204
|
||||
#: allianceauth/groupmanagement/views.py:235
|
||||
#, python-format
|
||||
msgid ""
|
||||
"An unhandled error occurred while processing the application from "
|
||||
"%(mainchar)s to %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:226
|
||||
#: allianceauth/groupmanagement/views.py:229
|
||||
#, python-format
|
||||
msgid "Rejected application from %(mainchar)s to %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:261
|
||||
#: allianceauth/groupmanagement/views.py:264
|
||||
#, python-format
|
||||
msgid "Accepted application from %(mainchar)s to leave %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:266
|
||||
#: allianceauth/groupmanagement/views.py:298
|
||||
#: allianceauth/groupmanagement/views.py:269
|
||||
#: allianceauth/groupmanagement/views.py:301
|
||||
#, python-format
|
||||
msgid ""
|
||||
"An unhandled error occurred while processing the application from "
|
||||
"%(mainchar)s to leave %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:292
|
||||
#: allianceauth/groupmanagement/views.py:295
|
||||
#, python-format
|
||||
msgid "Rejected application from %(mainchar)s to leave %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:336
|
||||
#: allianceauth/groupmanagement/views.py:346
|
||||
#: allianceauth/groupmanagement/views.py:339
|
||||
#: allianceauth/groupmanagement/views.py:349
|
||||
msgid "You cannot join that group"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:341
|
||||
#: allianceauth/groupmanagement/views.py:344
|
||||
msgid "You are already a member of that group."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:358
|
||||
#: allianceauth/groupmanagement/views.py:361
|
||||
msgid "You already have a pending application for that group."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:367
|
||||
#: allianceauth/groupmanagement/views.py:370
|
||||
#, python-format
|
||||
msgid "Applied to group %(group)s."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:377
|
||||
#: allianceauth/groupmanagement/views.py:380
|
||||
msgid "You cannot leave that group"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:381
|
||||
#: allianceauth/groupmanagement/views.py:384
|
||||
msgid "You are not a member of that group"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:393
|
||||
#: allianceauth/groupmanagement/views.py:396
|
||||
msgid "You already have a pending leave request for that group."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:409
|
||||
#: allianceauth/groupmanagement/views.py:412
|
||||
#, python-format
|
||||
msgid "Applied to leave group %(group)s."
|
||||
msgstr ""
|
||||
@@ -1074,16 +1112,6 @@ msgstr ""
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:28
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:127
|
||||
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:27
|
||||
#: allianceauth/hrapplications/templates/hrapplications/view.html:73
|
||||
#: allianceauth/srp/templates/srp/data.html:101
|
||||
#: allianceauth/srp/templates/srp/management.html:44
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:38
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
|
||||
#: allianceauth/hrapplications/templates/hrapplications/management.html:143
|
||||
@@ -1422,10 +1450,6 @@ msgstr ""
|
||||
msgid "Code Name"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:41
|
||||
msgid "States"
|
||||
msgstr ""
|
||||
@@ -2146,11 +2170,11 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/templates/allianceauth/admin-status/overview.html:95
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" %(queue_length)s queued tasks\n"
|
||||
" "
|
||||
msgid "running"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/templates/allianceauth/admin-status/overview.html:96
|
||||
msgid "queued"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
|
||||
@@ -2166,11 +2190,11 @@ msgid "AA Support Discord"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:10
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:14
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:16
|
||||
msgid "User Menu"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:56
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:67
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
@@ -2226,22 +2250,30 @@ msgid "Objective"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:64
|
||||
msgid "Days Remaining"
|
||||
msgid "Absolute Timer"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:65
|
||||
msgid "Hours Remaining"
|
||||
msgid "Date and Time"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:66
|
||||
msgid "Days Remaining"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:67
|
||||
msgid "Minutes Remaining"
|
||||
msgid "Hours Remaining"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:69
|
||||
msgid "Minutes Remaining"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:71
|
||||
msgid "Important"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/form.py:70
|
||||
#: allianceauth/timerboard/form.py:72
|
||||
msgid "Corp-Restricted"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Binary file not shown.
@@ -4,11 +4,11 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# frank1210 <francolopez_16@hotmail.com>, 2021
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2021
|
||||
# Young Anexo, 2023
|
||||
# Fegpawn Kaundur, 2023
|
||||
# frank1210 <francolopez_16@hotmail.com>, 2023
|
||||
# trenus, 2023
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2023
|
||||
# Young Anexo, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -16,8 +16,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: trenus, 2023\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Young Anexo, 2023\n"
|
||||
"Language-Team: Spanish (https://app.transifex.com/alliance-auth/teams/107430/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
Binary file not shown.
@@ -4,14 +4,14 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2020
|
||||
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2020
|
||||
# Keven D. <theenarki@gmail.com>, 2020
|
||||
# Idea ., 2021
|
||||
# Mickael PATTE, 2021
|
||||
# Geoffrey Fabbro, 2021
|
||||
# Keven D. <theenarki@gmail.com>, 2023
|
||||
# rockclodbuster, 2023
|
||||
# Geoffrey Fabbro, 2023
|
||||
# Mohssine Daghghar, 2023
|
||||
# Ludovick Fortin, 2023
|
||||
# François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2023
|
||||
# Mickael PATTE, 2023
|
||||
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2023
|
||||
# Idea ., 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -19,8 +19,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: Ludovick Fortin, 2023\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Idea ., 2023\n"
|
||||
"Language-Team: French (France) (https://app.transifex.com/alliance-auth/teams/107430/fr_FR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -4,8 +4,9 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# Foch Petain <brigadier.rockforward@gmail.com>, 2020
|
||||
# Foch Petain <brigadier.rockforward@gmail.com>, 2023
|
||||
# kotaneko, 2023
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -13,8 +14,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: kotaneko, 2023\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2023\n"
|
||||
"Language-Team: Japanese (https://app.transifex.com/alliance-auth/teams/107430/ja/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
Binary file not shown.
@@ -4,13 +4,13 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# None None <khd1226543@gmail.com>, 2020
|
||||
# Seowon Jung <seowon@hawaii.edu>, 2020
|
||||
# Olgeda Choi <undead.choi@gmail.com>, 2020
|
||||
# Lahty <js03js70@gmail.com>, 2020
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2020
|
||||
# ThatRagingKid, 2022
|
||||
# jackfrost, 2022
|
||||
# None None <khd1226543@gmail.com>, 2023
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2023
|
||||
# Seowon Jung <seowon@hawaii.edu>, 2023
|
||||
# Olgeda Choi <undead.choi@gmail.com>, 2023
|
||||
# ThatRagingKid, 2023
|
||||
# Lahty <js03js70@gmail.com>, 2023
|
||||
# jackfrost, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -18,8 +18,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: jackfrost, 2022\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: jackfrost, 2023\n"
|
||||
"Language-Team: Korean (Korea) (https://app.transifex.com/alliance-auth/teams/107430/ko_KR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
Binary file not shown.
@@ -4,9 +4,9 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# Alexander Gess <de.alex.gess@gmail.com>, 2020
|
||||
# Yuriy K <thedjcooltv@gmail.com>, 2020
|
||||
# Андрей Зубков <and.vareba81@gmail.com>, 2020
|
||||
# Yuriy K <thedjcooltv@gmail.com>, 2023
|
||||
# Андрей Зубков <and.vareba81@gmail.com>, 2023
|
||||
# Alexander Gess <de.alex.gess@gmail.com>, 2023
|
||||
# Filipp Chertiev <f@fzfx.ru>, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
@@ -15,7 +15,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Filipp Chertiev <f@fzfx.ru>, 2023\n"
|
||||
"Language-Team: Russian (https://app.transifex.com/alliance-auth/teams/107430/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
Binary file not shown.
@@ -4,6 +4,7 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# Denys Ivchenko, 2023
|
||||
# Kristof Swensen, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
@@ -12,7 +13,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Kristof Swensen, 2023\n"
|
||||
"Language-Team: Ukrainian (https://app.transifex.com/alliance-auth/teams/107430/uk/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -32,7 +33,7 @@ msgstr "Google Analytics V4"
|
||||
#: allianceauth/authentication/decorators.py:37
|
||||
msgid "A main character is required to perform that action. Add one below."
|
||||
msgstr ""
|
||||
"Для виконання цієї дії потрібен головний персонаж. Додайте його нижче."
|
||||
"Для виконання цієї дії потрібен основний персонаж. Додайте його нижче."
|
||||
|
||||
#: allianceauth/authentication/forms.py:12
|
||||
msgid "Email"
|
||||
@@ -124,7 +125,7 @@ msgstr "Додати персонажа"
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:115
|
||||
msgid "Change Main"
|
||||
msgstr "Змінити головного персонажа"
|
||||
msgstr "Змінити основного персонажа"
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/dashboard.html:125
|
||||
msgid "Group Memberships"
|
||||
@@ -352,7 +353,7 @@ msgstr "Не вдалося зібрати статистику корпорац
|
||||
|
||||
#: allianceauth/fleetactivitytracking/auth_hooks.py:9
|
||||
msgid "Fleet Activity Tracking"
|
||||
msgstr "Відстеження активності флоту"
|
||||
msgstr "Відстеження активності флотів"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/forms.py:6 allianceauth/srp/form.py:8
|
||||
#: allianceauth/srp/templates/srp/management.html:35
|
||||
@@ -456,7 +457,7 @@ msgstr "Корабель"
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:202
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:375
|
||||
msgid "Eve Time"
|
||||
msgstr "Час в грі"
|
||||
msgstr "Ігровий час"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:33
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:36
|
||||
@@ -560,16 +561,16 @@ msgstr "Fats"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:4
|
||||
msgid "Fatlink Corp Statistics"
|
||||
msgstr "Статистика корпорації Fatlink"
|
||||
msgstr "Статистика фатів корпорації"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:24
|
||||
msgid "Average fats"
|
||||
msgstr "Середній показник fats"
|
||||
msgstr "Середній показник фатів"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:4
|
||||
msgid "Fatlink statistics"
|
||||
msgstr "Статистика Fatlink"
|
||||
msgstr "Статистика фатів"
|
||||
|
||||
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:20
|
||||
msgid "Ticker"
|
||||
@@ -625,7 +626,7 @@ msgid ""
|
||||
"Cannot register the fleet participation for {character.character_name}. The "
|
||||
"character needs to be online."
|
||||
msgstr ""
|
||||
"Не можна зареєструвати участь в флоті для {character.character_name}. "
|
||||
"Не вдалося зареєструвати участь в флоті для {character.character_name}. "
|
||||
"Персонаж повинен бути в мережі."
|
||||
|
||||
#: allianceauth/groupmanagement/auth_hooks.py:17
|
||||
@@ -659,8 +660,7 @@ msgstr ""
|
||||
#: allianceauth/groupmanagement/models.py:113
|
||||
msgid "Group is hidden from users but can still join with the correct link."
|
||||
msgstr ""
|
||||
"Група прихована від користувачів, але можна приєднатися з правильним "
|
||||
"посиланням."
|
||||
"Група прихована від користувачів, але можна приєднатися за посиланням."
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:119
|
||||
msgid ""
|
||||
@@ -1045,7 +1045,7 @@ msgstr "Ви вже є членом цієї групи."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:358
|
||||
msgid "You already have a pending application for that group."
|
||||
msgstr "У вас вже є очікуюча заявка на вступ до цієї групи."
|
||||
msgstr "Ви вже подали заявку на вступ до цієї групи."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:367
|
||||
#, python-format
|
||||
@@ -1062,7 +1062,7 @@ msgstr "Ви не є учасником цієї групи"
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:393
|
||||
msgid "You already have a pending leave request for that group."
|
||||
msgstr "Ви вже маєте очікувану запит на вихід з цієї групи."
|
||||
msgstr "Ви вже подали запит на вихід з цієї групи."
|
||||
|
||||
#: allianceauth/groupmanagement/views.py:409
|
||||
#, python-format
|
||||
@@ -1321,7 +1321,7 @@ msgstr "Всі прочитані повідомлення видалено."
|
||||
|
||||
#: allianceauth/optimer/auth_hooks.py:10
|
||||
msgid "Fleet Operations"
|
||||
msgstr "Операції флоту"
|
||||
msgstr "Флотові операції"
|
||||
|
||||
#: allianceauth/optimer/form.py:12
|
||||
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11
|
||||
@@ -1345,7 +1345,7 @@ msgstr "Тип операції"
|
||||
#: allianceauth/optimer/form.py:17
|
||||
#: allianceauth/srp/templates/srp/management.html:38
|
||||
msgid "Fleet Commander"
|
||||
msgstr "Командувач флоту"
|
||||
msgstr "Командир флоту"
|
||||
|
||||
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
|
||||
#: allianceauth/srp/templates/srp/data.html:91
|
||||
@@ -1400,7 +1400,7 @@ msgstr "Немає наступних таймерів."
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:33
|
||||
msgid "Past Fleet Operations"
|
||||
msgstr "Минулі флотові операції"
|
||||
msgstr "Завершені флотові операції"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:37
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:535
|
||||
@@ -1484,7 +1484,7 @@ msgstr "Стани"
|
||||
|
||||
#: allianceauth/services/abstract.py:72
|
||||
msgid "That service account already exists"
|
||||
msgstr "Такий обліковий запис сервісу вже існує"
|
||||
msgstr "Такий сервісний обліковий запис вже існує"
|
||||
|
||||
#: allianceauth/services/abstract.py:103
|
||||
#, python-brace-format
|
||||
@@ -1505,7 +1505,7 @@ msgstr "Командир флоту:"
|
||||
|
||||
#: allianceauth/services/forms.py:8
|
||||
msgid "Fleet Comms:"
|
||||
msgstr "Комунікації флоту:"
|
||||
msgstr "Голосовий канал флоту:"
|
||||
|
||||
#: allianceauth/services/forms.py:9
|
||||
msgid "Fleet Type:"
|
||||
@@ -1545,7 +1545,7 @@ msgstr "Ні"
|
||||
|
||||
#: allianceauth/services/forms.py:16
|
||||
msgid "Important?*"
|
||||
msgstr "Важливо?*"
|
||||
msgstr "Важливий?*"
|
||||
|
||||
#: allianceauth/services/forms.py:21 allianceauth/services/forms.py:31
|
||||
msgid "Password"
|
||||
@@ -1614,7 +1614,7 @@ msgstr "Ви не маєте прав на доступ до Discourse."
|
||||
|
||||
#: allianceauth/services/modules/discourse/views.py:34
|
||||
msgid "You must have a main character set to access Discourse."
|
||||
msgstr "Ви повинні мати головний персонаж, щоб отримати доступ до Discourse."
|
||||
msgstr "Ви повинні мати основний персонаж, щоб отримати доступ до Discourse."
|
||||
|
||||
#: allianceauth/services/modules/discourse/views.py:44
|
||||
msgid ""
|
||||
@@ -1702,7 +1702,7 @@ msgstr "Відправлено трансляцію Jabber на %s"
|
||||
|
||||
#: allianceauth/services/modules/openfire/views.py:144
|
||||
msgid "Set jabber password."
|
||||
msgstr "Встановлення пароля Jabber."
|
||||
msgstr "Встановити пароль Jabber."
|
||||
|
||||
#: allianceauth/services/modules/phpbb3/views.py:34
|
||||
msgid "Activated forum account."
|
||||
@@ -1713,7 +1713,7 @@ msgstr "Активований обліковий запис форуму."
|
||||
#: allianceauth/services/modules/phpbb3/views.py:78
|
||||
#: allianceauth/services/modules/phpbb3/views.py:101
|
||||
msgid "An error occurred while processing your forum account."
|
||||
msgstr "Виникла помилка під час обробки вашого облікового запису форуму."
|
||||
msgstr "Виникла помилка під час обробки вашого облікового запису на форумі."
|
||||
|
||||
#: allianceauth/services/modules/phpbb3/views.py:53
|
||||
msgid "Deactivated forum account."
|
||||
@@ -1721,11 +1721,11 @@ msgstr "Деактивований обліковий запис форуму."
|
||||
|
||||
#: allianceauth/services/modules/phpbb3/views.py:70
|
||||
msgid "Reset forum password."
|
||||
msgstr "Скидання пароля форуму."
|
||||
msgstr "Скинути пароль форуму."
|
||||
|
||||
#: allianceauth/services/modules/phpbb3/views.py:98
|
||||
msgid "Set forum password."
|
||||
msgstr "Встановлення пароля форуму."
|
||||
msgstr "Встановити пароль форуму."
|
||||
|
||||
#: allianceauth/services/modules/smf/views.py:52
|
||||
msgid "Activated SMF account."
|
||||
@@ -1744,11 +1744,11 @@ msgstr "Деактивований обліковий запис SMF."
|
||||
|
||||
#: allianceauth/services/modules/smf/views.py:95
|
||||
msgid "Reset SMF password."
|
||||
msgstr "Скидання пароля SMF."
|
||||
msgstr "Скинути пароль SMF."
|
||||
|
||||
#: allianceauth/services/modules/smf/views.py:121
|
||||
msgid "Set SMF password."
|
||||
msgstr "Встановлення пароля SMF."
|
||||
msgstr "Встановити пароль SMF."
|
||||
|
||||
#: allianceauth/services/modules/teamspeak3/forms.py:14
|
||||
#, python-format
|
||||
@@ -1761,7 +1761,7 @@ msgstr "Оновити групи TS3"
|
||||
|
||||
#: allianceauth/services/modules/teamspeak3/templates/services/teamspeak3/teamspeakjoin.html:5
|
||||
msgid "Verify Teamspeak"
|
||||
msgstr "Перевірте Teamspeak"
|
||||
msgstr "Перевірити Teamspeak"
|
||||
|
||||
#: allianceauth/services/modules/teamspeak3/templates/services/teamspeak3/teamspeakjoin.html:10
|
||||
msgid "Verify Teamspeak Identity"
|
||||
@@ -1869,11 +1869,11 @@ msgstr "Керування послугами"
|
||||
|
||||
#: allianceauth/services/templates/services/services.html:9
|
||||
msgid "Available Services"
|
||||
msgstr "Доступні послуги"
|
||||
msgstr "Доступні сервіси"
|
||||
|
||||
#: allianceauth/services/templates/services/services.html:14
|
||||
msgid "Service"
|
||||
msgstr "Послуга"
|
||||
msgstr "Сервіс"
|
||||
|
||||
#: allianceauth/services/templates/services/services.html:16
|
||||
msgid "Domain"
|
||||
@@ -1881,7 +1881,7 @@ msgstr "Домен"
|
||||
|
||||
#: allianceauth/srp/auth_hooks.py:13
|
||||
msgid "Ship Replacement"
|
||||
msgstr "Компенсація за корабель"
|
||||
msgstr "Компенсації"
|
||||
|
||||
#: allianceauth/srp/form.py:9
|
||||
#: allianceauth/srp/templates/srp/management.html:36
|
||||
|
||||
Binary file not shown.
@@ -4,9 +4,10 @@
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
# Translators:
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2020
|
||||
# Jesse . <sgeine@hotmail.com>, 2020
|
||||
# Aaron BuBu <351793078@qq.com>, 2020
|
||||
# Jesse . <sgeine@hotmail.com>, 2023
|
||||
# Aaron BuBu <351793078@qq.com>, 2023
|
||||
# Joel Falknau <ozirascal@gmail.com>, 2023
|
||||
# Shen Yang, 2023
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
@@ -14,8 +15,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-09 18:20+1000\n"
|
||||
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
|
||||
"Last-Translator: Aaron BuBu <351793078@qq.com>, 2020\n"
|
||||
"PO-Revision-Date: 2023-10-08 09:23+0000\n"
|
||||
"Last-Translator: Shen Yang, 2023\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/alliance-auth/teams/107430/zh-Hans/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -46,48 +47,48 @@ msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:80
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
msgstr "英语"
|
||||
|
||||
#: allianceauth/authentication/models.py:81
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
msgstr "德语"
|
||||
|
||||
#: allianceauth/authentication/models.py:82
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
msgstr "西班牙语"
|
||||
|
||||
#: allianceauth/authentication/models.py:83
|
||||
msgid "Chinese Simplified"
|
||||
msgstr ""
|
||||
msgstr "简体中文"
|
||||
|
||||
#: allianceauth/authentication/models.py:84
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
msgstr "俄语"
|
||||
|
||||
#: allianceauth/authentication/models.py:85
|
||||
msgid "Korean"
|
||||
msgstr ""
|
||||
msgstr "韩语"
|
||||
|
||||
#: allianceauth/authentication/models.py:86
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
msgstr "法语"
|
||||
|
||||
#: allianceauth/authentication/models.py:87
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
msgstr "日语"
|
||||
|
||||
#: allianceauth/authentication/models.py:88
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
msgstr "意大利语"
|
||||
|
||||
#: allianceauth/authentication/models.py:91
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
msgstr "语言"
|
||||
|
||||
#: allianceauth/authentication/models.py:96
|
||||
#: allianceauth/templates/allianceauth/night-toggle.html:6
|
||||
msgid "Night Mode"
|
||||
msgstr ""
|
||||
msgstr "夜间模式"
|
||||
|
||||
#: allianceauth/authentication/models.py:110
|
||||
#, python-format
|
||||
@@ -696,7 +697,7 @@ msgstr ""
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:215
|
||||
msgid "reason"
|
||||
msgstr ""
|
||||
msgstr "原因"
|
||||
|
||||
#: allianceauth/groupmanagement/models.py:215
|
||||
msgid "Reason why this name is reserved."
|
||||
@@ -754,7 +755,7 @@ msgstr "操作者"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:48
|
||||
msgid "Removed"
|
||||
msgstr ""
|
||||
msgstr "已移除"
|
||||
|
||||
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:60
|
||||
msgid "All times displayed are EVE/UTC."
|
||||
@@ -1198,11 +1199,11 @@ msgstr "添加评论"
|
||||
|
||||
#: allianceauth/notifications/models.py:21
|
||||
msgid "danger"
|
||||
msgstr ""
|
||||
msgstr "危险"
|
||||
|
||||
#: allianceauth/notifications/models.py:22
|
||||
msgid "warning"
|
||||
msgstr ""
|
||||
msgstr "警告"
|
||||
|
||||
#: allianceauth/notifications/models.py:23
|
||||
msgid "info"
|
||||
@@ -1343,7 +1344,7 @@ msgstr "当前EVE游戏内时间"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:26
|
||||
msgid "Next Fleet Operations"
|
||||
msgstr ""
|
||||
msgstr "下一个舰队任务"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:30
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:362
|
||||
@@ -1352,7 +1353,7 @@ msgstr "没有快到的时间节点,歇一会吧"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:33
|
||||
msgid "Past Fleet Operations"
|
||||
msgstr ""
|
||||
msgstr "过去的舰队任务"
|
||||
|
||||
#: allianceauth/optimer/templates/optimer/management.html:37
|
||||
#: allianceauth/timerboard/templates/timerboard/view.html:535
|
||||
@@ -2257,15 +2258,15 @@ msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/models.py:15
|
||||
msgid "Shield"
|
||||
msgstr ""
|
||||
msgstr "护盾"
|
||||
|
||||
#: allianceauth/timerboard/models.py:16
|
||||
msgid "Armor"
|
||||
msgstr ""
|
||||
msgstr "装甲"
|
||||
|
||||
#: allianceauth/timerboard/models.py:17
|
||||
msgid "Hull"
|
||||
msgstr ""
|
||||
msgstr "结构"
|
||||
|
||||
#: allianceauth/timerboard/models.py:18
|
||||
msgid "Final"
|
||||
@@ -2273,11 +2274,11 @@ msgstr ""
|
||||
|
||||
#: allianceauth/timerboard/models.py:19
|
||||
msgid "Anchoring"
|
||||
msgstr ""
|
||||
msgstr "铆钉"
|
||||
|
||||
#: allianceauth/timerboard/models.py:20
|
||||
msgid "Unanchoring"
|
||||
msgstr ""
|
||||
msgstr "解锚"
|
||||
|
||||
#: allianceauth/timerboard/templates/timerboard/timer_confirm_delete.html:11
|
||||
msgid "Delete Timer"
|
||||
|
||||
@@ -13,6 +13,10 @@ app = Celery('{{ project_name }}')
|
||||
# the configuration object to child processes.
|
||||
app.config_from_object('django.conf:settings')
|
||||
|
||||
# Automatically try to establish the connection to the AMQP broker on
|
||||
# Celery startup if it is unavailable.
|
||||
app.conf.broker_connection_retry_on_startup = True
|
||||
|
||||
# setup priorities ( 0 Highest, 9 Lowest )
|
||||
app.conf.broker_transport_options = {
|
||||
'priority_steps': list(range(10)), # setup que to have 10 steps
|
||||
|
||||
@@ -6,6 +6,7 @@ from ...admin import ServicesUserAdmin
|
||||
from . import __title__
|
||||
from .models import DiscordUser
|
||||
from .utils import LoggerAddTag
|
||||
from .auth_hooks import DiscordService
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
@@ -27,6 +28,6 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
||||
|
||||
@admin.display(description='Discord Username', ordering='username')
|
||||
def _username(self, obj):
|
||||
if obj.username and obj.discriminator:
|
||||
return f'{obj.username}#{obj.discriminator}'
|
||||
return ''
|
||||
return DiscordService.get_discord_username(
|
||||
username=obj.username, discriminator=obj.discriminator
|
||||
)
|
||||
|
||||
@@ -11,6 +11,9 @@ from .models import DiscordUser
|
||||
from .urls import urlpatterns
|
||||
from .utils import LoggerAddTag
|
||||
from . import tasks, __title__
|
||||
from .app_settings import (
|
||||
DISCORD_SYNC_NAMES
|
||||
)
|
||||
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
@@ -30,6 +33,29 @@ class DiscordService(ServicesHook):
|
||||
self.access_perm = 'discord.access_discord'
|
||||
self.name_format = '{character_name}'
|
||||
|
||||
@staticmethod
|
||||
def get_discord_username(username:str, discriminator:str) -> str:
|
||||
"""
|
||||
Determine the Discord username (Old and new format)
|
||||
:param username:
|
||||
:type username:
|
||||
:param discriminator:
|
||||
:type discriminator:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
if username and discriminator:
|
||||
discord_username = f'{username}#{discriminator}'
|
||||
|
||||
# New Discord user name format
|
||||
if discriminator == '0':
|
||||
discord_username = f'@{username}'
|
||||
else:
|
||||
discord_username = ''
|
||||
|
||||
return discord_username
|
||||
|
||||
def delete_user(self, user: User, notify_user: bool = False) -> None:
|
||||
if self.user_has_account(user):
|
||||
logger.debug('Deleting user %s %s account', user, self.name)
|
||||
@@ -43,10 +69,19 @@ class DiscordService(ServicesHook):
|
||||
user_has_account = True
|
||||
username = request.user.discord.username
|
||||
discriminator = request.user.discord.discriminator
|
||||
if username and discriminator:
|
||||
discord_username = f'{username}#{discriminator}'
|
||||
else:
|
||||
discord_username = ''
|
||||
|
||||
discord_username = self.get_discord_username(
|
||||
username=username, discriminator=discriminator
|
||||
)
|
||||
|
||||
# if username and discriminator:
|
||||
# discord_username = f'{username}#{discriminator}'
|
||||
#
|
||||
# # New Discord user name format
|
||||
# if discriminator == '0':
|
||||
# discord_username = f'@{username}'
|
||||
# else:
|
||||
# discord_username = ''
|
||||
else:
|
||||
discord_username = ''
|
||||
user_has_account = False
|
||||
@@ -67,17 +102,18 @@ class DiscordService(ServicesHook):
|
||||
return has_perms
|
||||
|
||||
def sync_nickname(self, user):
|
||||
logger.debug('Syncing %s nickname for user %s', self.name, user)
|
||||
if self.user_has_account(user):
|
||||
tasks.update_nickname.apply_async(
|
||||
kwargs={
|
||||
'user_pk': user.pk,
|
||||
# since the new nickname is not yet in the DB we need to
|
||||
# provide it manually to the task
|
||||
'nickname': user_formatted_nick(user)
|
||||
},
|
||||
priority=SINGLE_TASK_PRIORITY
|
||||
)
|
||||
if DISCORD_SYNC_NAMES:
|
||||
logger.debug('Syncing %s nickname for user %s', self.name, user)
|
||||
if self.user_has_account(user):
|
||||
tasks.update_nickname.apply_async(
|
||||
kwargs={
|
||||
'user_pk': user.pk,
|
||||
# since the new nickname is not yet in the DB we need to
|
||||
# provide it manually to the task
|
||||
'nickname': user_formatted_nick(user)
|
||||
},
|
||||
priority=SINGLE_TASK_PRIORITY
|
||||
)
|
||||
|
||||
def sync_nicknames_bulk(self, users: list):
|
||||
"""Sync nickname for a list of users in bulk.
|
||||
|
||||
@@ -81,11 +81,18 @@ class TestDiscordService(NoSocketsTestCase):
|
||||
self.assertFalse(DiscordUser.objects.filter(user=self.none_member).exists())
|
||||
|
||||
@patch(MODULE_PATH + '.tasks.update_nickname')
|
||||
@patch(MODULE_PATH + '.auth_hooks.DISCORD_SYNC_NAMES', True)
|
||||
def test_sync_nickname(self, mock_update_nickname):
|
||||
service = self.service()
|
||||
service.sync_nickname(self.member)
|
||||
self.assertTrue(mock_update_nickname.apply_async.called)
|
||||
|
||||
@patch(MODULE_PATH + '.tasks.update_nickname')
|
||||
def test_sync_nickname_no_setting(self, mock_update_nickname):
|
||||
service = self.service()
|
||||
service.sync_nickname(self.member)
|
||||
self.assertFalse(mock_update_nickname.apply_async.called)
|
||||
|
||||
@patch(MODULE_PATH + '.tasks.update_nicknames_bulk')
|
||||
def test_sync_nicknames_bulk(self, mock_update_nicknames_bulk):
|
||||
service = self.service()
|
||||
@@ -150,3 +157,23 @@ class TestDiscordService(NoSocketsTestCase):
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn('/discord/reset/', response)
|
||||
self.assertIn('/discord/deactivate/', response)
|
||||
|
||||
def test_new_discord_username_format(self):
|
||||
"""
|
||||
Test if we get Discord's new username format
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
# given
|
||||
username = 'william_riker'
|
||||
discriminator = '0' # Seems to be returned as 0 for Discord's new username format
|
||||
|
||||
# when
|
||||
discord_username = DiscordService.get_discord_username(
|
||||
username=username, discriminator=discriminator
|
||||
)
|
||||
|
||||
# then
|
||||
expected_username = '@william_riker'
|
||||
self.assertEqual(first=discord_username, second=expected_username)
|
||||
|
||||
@@ -164,6 +164,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
self.discord_user = DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
|
||||
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
|
||||
|
||||
@patch(MODULE_PATH + '.auth_hooks.DISCORD_SYNC_NAMES', True)
|
||||
def test_when_name_of_main_changes_then_discord_nick_is_updated(
|
||||
self, requests_mocker
|
||||
):
|
||||
@@ -185,6 +186,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
self.assertTrue(nick_updated)
|
||||
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
|
||||
|
||||
@patch(MODULE_PATH + '.auth_hooks.DISCORD_SYNC_NAMES', True)
|
||||
def test_when_name_of_main_changes_and_user_deleted_then_account_is_deleted(
|
||||
self, requests_mocker
|
||||
):
|
||||
|
||||
@@ -92,12 +92,8 @@
|
||||
{% 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
|
||||
{% endblocktranslate %}
|
||||
<span id="task-counts">?</span> {% translate 'running' %} |
|
||||
<span id="queued-tasks-count">?</span> {% translate 'queued' %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,3 +101,36 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const elemRunning = document.getElementById("task-counts");
|
||||
const elemQueued = document.getElementById("queued-tasks-count");
|
||||
|
||||
fetch('{% url "authentication:task_counts" %}')
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error("Something went wrong");
|
||||
})
|
||||
.then((responseJson) => {
|
||||
const running = responseJson.tasks_running;
|
||||
if (running == null) {
|
||||
elemRunning.textContent = "N/A";
|
||||
} else {
|
||||
elemRunning.textContent = running.toLocaleString();
|
||||
}
|
||||
|
||||
const queued = responseJson.tasks_queued;
|
||||
if (queued == null) {
|
||||
elemQueued.textContent = "N/A";
|
||||
} else {
|
||||
elemQueued.textContent = queued.toLocaleString();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
elemRunning.textContent = "ERROR";
|
||||
elemQueued.textContent = "ERROR";
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load navactive %}
|
||||
{% load auth_notifications %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import amqp.exceptions
|
||||
import requests
|
||||
from celery.app import app_or_default
|
||||
from packaging.version import InvalidVersion, Version as Pep440Version
|
||||
|
||||
from django import template
|
||||
@@ -11,8 +8,9 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from allianceauth import __version__
|
||||
|
||||
from ..authentication.task_statistics.counters import dashboard_results
|
||||
from allianceauth.authentication.task_statistics.counters import (
|
||||
dashboard_results,
|
||||
)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -48,18 +46,15 @@ def status_overview() -> dict:
|
||||
response = {
|
||||
"notifications": list(),
|
||||
"current_version": __version__,
|
||||
"task_queue_length": None,
|
||||
"tasks_succeeded": 0,
|
||||
"tasks_retried": 0,
|
||||
"tasks_failed": 0,
|
||||
"tasks_total": 0,
|
||||
"tasks_hours": 0,
|
||||
"earliest_task": None,
|
||||
"tasks_running": 0
|
||||
}
|
||||
response.update(_current_notifications())
|
||||
response.update(_current_version_summary())
|
||||
response.update({'task_queue_length': _fetch_celery_queue_length()})
|
||||
response.update(_celery_stats())
|
||||
return response
|
||||
|
||||
@@ -74,27 +69,9 @@ def _celery_stats() -> dict:
|
||||
"tasks_total": results.total,
|
||||
"tasks_hours": results.hours,
|
||||
"earliest_task": results.earliest_task,
|
||||
"tasks_running": results.running,
|
||||
}
|
||||
|
||||
|
||||
def _fetch_celery_queue_length() -> Optional[int]:
|
||||
try:
|
||||
app = app_or_default(None)
|
||||
with app.connection_or_acquire() as conn:
|
||||
result = conn.default_channel.queue_declare(
|
||||
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'),
|
||||
passive=True
|
||||
)
|
||||
return result.message_count
|
||||
except amqp.exceptions.ChannelError:
|
||||
# Queue doesn't exist, probably empty
|
||||
return 0
|
||||
except Exception:
|
||||
logger.exception("Failed to get celery queue length")
|
||||
return None
|
||||
|
||||
|
||||
def _current_notifications() -> dict:
|
||||
"""returns the newest 5 announcement issues"""
|
||||
try:
|
||||
|
||||
65
allianceauth/utils/counters.py
Normal file
65
allianceauth/utils/counters.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Counters."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
from .cache import get_redis_client
|
||||
|
||||
|
||||
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() 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)
|
||||
120
allianceauth/utils/tests/test_counters.py
Normal file
120
allianceauth/utils/tests/test_counters.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from allianceauth.utils.counters import ItemCounter
|
||||
|
||||
MODULE_PATH = "allianceauth.utils.counters"
|
||||
|
||||
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)
|
||||
@@ -1,7 +1,7 @@
|
||||
PROTOCOL=https://
|
||||
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
|
||||
DOMAIN=%DOMAIN%
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v3.6.1
|
||||
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v3.8.0
|
||||
|
||||
# Nginx Proxy Manager
|
||||
PROXY_HTTP_PORT=80
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM python:3.9-slim
|
||||
ARG AUTH_VERSION=v3.6.1
|
||||
ARG AUTH_VERSION=v3.8.0
|
||||
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV AUTH_USER=allianceauth
|
||||
|
||||
49
docs/development/aa_core/code-style
Normal file
49
docs/development/aa_core/code-style
Normal file
@@ -0,0 +1,49 @@
|
||||
# Code Style
|
||||
|
||||
## Pre-Commit
|
||||
|
||||
Alliance Auth is a team effort with developers of various skill levels and background. To avoid significant drift or formatting changes between developers we use [pre-commit](https://pre-commit.com/) to apply a very minimal set of formatting checks to code contributed to the project.
|
||||
|
||||
Pre-commit is also very popular with our Community Apps and may be significantly more opinionated or looser depending on the project.
|
||||
|
||||
To get started, `pip install pre-commit`, then `pre-commit install` to add the git hooks.
|
||||
|
||||
Before any code is "git push"-ed, pre-commit will check it for uniformity and correct it if possible
|
||||
|
||||
```bash
|
||||
check python ast.....................................(no files to check)Skipped
|
||||
check yaml...........................................(no files to check)Skipped
|
||||
check json...........................................(no files to check)Skipped
|
||||
check toml...........................................(no files to check)Skipped
|
||||
check xml............................................(no files to check)Skipped
|
||||
check for merge conflicts............................(no files to check)Skipped
|
||||
check for added large files..........................(no files to check)Skipped
|
||||
detect private key...................................(no files to check)Skipped
|
||||
check for case conflicts.............................(no files to check)Skipped
|
||||
debug statements (python)............................(no files to check)Skipped
|
||||
fix python encoding pragma...........................(no files to check)Skipped
|
||||
fix utf-8 byte order marker..........................(no files to check)Skipped
|
||||
mixed line ending....................................(no files to check)Skipped
|
||||
trim trailing whitespace.............................(no files to check)Skipped
|
||||
check that executables have shebangs.................(no files to check)Skipped
|
||||
fix end of files.....................................(no files to check)Skipped
|
||||
Check .editorconfig rules............................(no files to check)Skipped
|
||||
django-upgrade.......................................(no files to check)Skipped
|
||||
pyupgrade............................................(no files to check)Skipped
|
||||
```
|
||||
|
||||
## Editorconfig
|
||||
|
||||
[Editorconfig](https://editorconfig.org/) is supported my most IDE's to streamline the most common editor disparities. While checked by our pre-commit file, using this in your IDE (Either automatically or via a plugin) will minimize the corrections that may need to be made.
|
||||
|
||||
## Doc Strings
|
||||
|
||||
We prefer either [PEP-287](https://peps.python.org/pep-0287/)/[reStructuredText](https://docutils.sourceforge.io/rst.html) or [Google](https://google.github.io/styleguide/pyguide.html#381-docstrings) Docstrings.
|
||||
|
||||
These can be used to automatically generate our Sphinx documentation in either format.
|
||||
|
||||
## Best Practice
|
||||
|
||||
It is advisable to avoid wide formatting changes on code that is not being modified by an MR. Further to this, automated code formatting should be kept to a minimal when modifying sections of existing files.
|
||||
|
||||
If you are contributing whole modules or rewriting large sections of code you may use any legible code formatting valid under Python.
|
||||
@@ -7,4 +7,5 @@ This section contains important information on how to develop Alliance Auth itse
|
||||
:maxdepth: 1
|
||||
|
||||
documentation
|
||||
code-style
|
||||
```
|
||||
|
||||
@@ -399,16 +399,10 @@ Update & install basic tools before installing further Python packages:
|
||||
pip install -U pip setuptools wheel
|
||||
```
|
||||
|
||||
You can install **Alliance Auth** with the following command. This will install AA and all its Python dependencies.
|
||||
You can install **Alliance Auth** with the following command. This will install AA, AA's Python dependencies, superlance for memory monitoring and gunicorn as a wsgi server
|
||||
|
||||
```bash
|
||||
pip install allianceauth
|
||||
```
|
||||
|
||||
You should also install Gunicorn now unless you want to use another WSGI server (see [Gunicorn](#gunicorn) for details):
|
||||
|
||||
```bash
|
||||
pip install gunicorn
|
||||
pip install allianceauth superlance gunicorn
|
||||
```
|
||||
|
||||
#### Create Alliance Auth project
|
||||
|
||||
@@ -44,7 +44,7 @@ You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup fo
|
||||
|
||||
## Install
|
||||
|
||||
Ubuntu 1804, 2004. 2204:
|
||||
Ubuntu 1804, 2004, 2204:
|
||||
```bash
|
||||
sudo apt-get install nginx
|
||||
```
|
||||
@@ -59,18 +59,13 @@ CentOS Stream 8, Stream 9:
|
||||
sudo dnf install nginx
|
||||
```
|
||||
|
||||
Create a config file in `/etc/nginx/sites-available` and call it `alliance-auth.conf` or whatever your preferred name is.
|
||||
Create a config file in `/etc/nginx/sites-available` (`/etc/nginx/conf.d` on CentOS) and call it `alliance-auth.conf` or whatever your preferred name is.
|
||||
|
||||
Create a symbolic link to enable the site
|
||||
Create a symbolic link to enable the site (not needed on CentOS):
|
||||
```bash
|
||||
ln -s /etc/nginx/sites-available/alliance-auth.conf /etc/nginx/sites-enabled/
|
||||
```
|
||||
|
||||
### CentOS
|
||||
|
||||
Create a config file in `/etc/nginx/conf.d` and call it `alliance-auth.conf` or whatever your preferred name is.
|
||||
|
||||
|
||||
### Basic config
|
||||
|
||||
Copy this basic config into your config file. Make whatever changes you feel are necessary.
|
||||
|
||||
@@ -28,43 +28,11 @@ command=/home/allianceserver/venv/auth/bin/celery -A myauth worker -l info
|
||||
|
||||
Celery workers often have memory leaks and will therefore grow in size over time. While the Alliance Auth team is working hard to ensure Auth is free of memory leaks some may still be cause by bugs in different versions of libraries or community apps. It is therefore good practice to enable features that protect against potential memory leaks.
|
||||
|
||||
There are two ways to protect against memory leaks:
|
||||
|
||||
- Worker
|
||||
- Supervisor
|
||||
|
||||
### Worker
|
||||
|
||||
Celery workers can be configured to automatically restart if they grow above a defined memory threshold. Restarts will be graceful, so current tasks will be allowed to complete before the restart happens.
|
||||
|
||||
To add protection against memory leaks add the following to the command configuration of your worker in the `supervisor.conf` file. This sets the upper limit to 256MB.
|
||||
|
||||
```text
|
||||
--max-memory-per-child 262144
|
||||
```
|
||||
|
||||
Full example:
|
||||
|
||||
```text
|
||||
command=/home/allianceserver/venv/auth/bin/celery -A myauth worker --max-memory-per-child 262144
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. hint::
|
||||
The 256 MB limit is just an example and should be adjusted to your system configuration. We would suggest to not go below 128MB though, since new workers start with around 80 MB already. Also take into consideration that this value is per worker and that you properly have more than one worker running in your system (if your workers run as processes, which is the default).
|
||||
The 256 MB limit is just an example and should be adjusted to your system configuration. We would suggest to not go below 128MB though, since new workers start with around 80 MB already. Also take into consideration that this value is per worker and that you may have more than one worker running in your system.
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. warning::
|
||||
The ``max-memory-per-child`` parameter only works when workers run as processes (which is the default). It does not work for threads.
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
Alternatively, you can also limit the number of runs per worker until a restart is performed with the worker parameter ``max-tasks-per-child``. This can also protect against memory leaks if you set the threshold is low enough. However, it is less precise since than using ``max-memory-per-child``.
|
||||
```
|
||||
|
||||
See also the [official Celery documentation](https://docs.celeryproject.org/en/stable/userguide/workers.html#max-memory-per-child-setting) for more information about these two worker parameters.
|
||||
|
||||
### Supervisor
|
||||
|
||||
@@ -78,35 +46,68 @@ To setup install superlance into your venv with:
|
||||
pip install superlance
|
||||
```
|
||||
|
||||
You can then add `memmon` to your `supervisor.conf`. Here is an example setup with a worker that runs with gevent:
|
||||
You can then add `memmon` to your `supervisor.conf`:
|
||||
|
||||
```text
|
||||
[eventlistener:memmon]
|
||||
command=/home/allianceserver/venv/auth/bin/memmon -p worker=512MB
|
||||
command=/home/allianceserver/venv/auth/bin/memmon -p worker=256MB
|
||||
directory=/home/allianceserver/myauth
|
||||
events=TICK_60
|
||||
```
|
||||
|
||||
This setup will check the memory consumption of the program "worker" every 60 secs and automatically restart it if is goes above 512 MB. Note that it will use the stop signal configured in supervisor, which is `TERM` by default. `TERM` will cause a "warm shutdown" of your worker, so all currently running tasks are completed before the restart.
|
||||
This setup will check the memory consumption of the program "worker" every 60 secs and automatically restart it if is goes above 256 MB. Note that it will use the stop signal configured in supervisor, which is `TERM` by default. `TERM` will cause a "warm shutdown" of your worker, so all currently running tasks are completed before the restart.
|
||||
|
||||
Again, the 512 MB is just an example and should be adjusted to fit your system configuration.
|
||||
Again, the 256 MB is just an example and should be adjusted to fit your system configuration.
|
||||
|
||||
## Increasing task throughput
|
||||
|
||||
Celery tasks are designed to run concurrently, so one obvious way to increase task throughput is run more tasks in parallel.
|
||||
Celery tasks are designed to run concurrently, so one obvious way to increase task throughput is run more tasks in parallel. The default celery worker configuration will allow either of these options to be configured out of the box.
|
||||
|
||||
### Extra Worker Threads
|
||||
|
||||
The easiest way to increate throughput can be achieved by increasing the `numprocs` parameter of the suprvisor process. For example:
|
||||
|
||||
```text
|
||||
[program:worker]
|
||||
...
|
||||
numprocs=2
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
...
|
||||
```
|
||||
|
||||
This number will be multiplied by your concurrency setting,
|
||||
|
||||
```
|
||||
numprocs * concurency = workers
|
||||
```
|
||||
|
||||
increasing this number will require a modification to the memmon settings as each `numproc` worker will get a unique name for example with `numproc=3`
|
||||
|
||||
```text
|
||||
[eventlistener:memmon]
|
||||
...
|
||||
command=... -p worker_00=256MB -p worker_01=256MB -p worker_02=256MB
|
||||
...
|
||||
```
|
||||
|
||||
```eval_rst
|
||||
.. hint::
|
||||
You will want to experiment with different settings to find the optimal. One way to generate task load and verify your configuration is to run a model update with the following command:
|
||||
|
||||
::
|
||||
|
||||
celery -A myauth call allianceauth.eveonline.tasks.run_model_update
|
||||
|
||||
```
|
||||
|
||||
### Concurrency
|
||||
|
||||
This can be achieved by the setting the concurrency parameter of the celery worker to a higher number. For example:
|
||||
|
||||
```text
|
||||
--concurrency=4
|
||||
--concurrency=10
|
||||
```
|
||||
|
||||
However, there is a catch: In the default configuration each worker will spawn as it's own process. So increasing the number of workers will increase both CPU load and memory consumption in your system.
|
||||
|
||||
The recommended number of workers is one per core, which is what you get automatically with the default configuration. Going beyond that can quickly reduce you overall system performance. i.e. the response time for Alliance Auth or other apps running on the same system may take a hit while many tasks are running.
|
||||
|
||||
```eval_rst
|
||||
.. hint::
|
||||
The optimal number will hugely depend on your individual system configuration and you may want to experiment with different settings to find the optimal. One way to generate task load and verify your configuration is to run a model update with the following command:
|
||||
@@ -117,43 +118,6 @@ The recommended number of workers is one per core, which is what you get automat
|
||||
|
||||
```
|
||||
|
||||
### Processes vs. Threads
|
||||
|
||||
A better way to increase concurrency without impacting is to switch from processes to threads for celery workers. In general celery workers perform better with processes when tasks are primarily CPU bound. And they perform better with threads when tasks that are primarily I/O bound.
|
||||
|
||||
Alliance Auth tasks are primarily I/O bound (most tasks are fetching data from ESI and/or updating the local database), so threads are clearly the better choice for Alliance Auth. However, there is a catch. Celery's out-of-the-box support for threads is limited and additional packages and configurations is required to make it work. Nonetheless, the performance gain - especially in smaller systems - is significant, so it may well be worth the additional configuration complexity.
|
||||
|
||||
```eval_rst
|
||||
.. warning::
|
||||
One important feature that no longer works with threads is the worker parameter ``--max-memory-per-child`` that protects against memory leaks. But you can alternatively use supervisor_ to monitor and restart your workers.
|
||||
```
|
||||
|
||||
See also the also [this guide](https://www.distributedpython.com/2018/10/26/celery-execution-pool/) on more information about how to configure the execution pool for workers.
|
||||
|
||||
### Setting up for threads
|
||||
|
||||
First, you need to install a threads packages. Celery supports both gevent and eventlet. We will go with gevent, since it's newer and better supported. Should you encounter any issues with gevent, you may want to try eventlet.
|
||||
|
||||
To install gevent make sure you are in your venv and install the following:
|
||||
|
||||
```bash
|
||||
pip install gevent
|
||||
```
|
||||
|
||||
Next we need to reconfigure the workers to use gevent threads. For that add the following parameters to your worker config:
|
||||
|
||||
```text
|
||||
--pool=gevent --concurrency=10
|
||||
```
|
||||
|
||||
Full example:
|
||||
|
||||
```text
|
||||
command=/home/allianceserver/venv/auth/bin/celery -A myauth worker --pool=gevent --concurrency=10
|
||||
```
|
||||
|
||||
Make sure to restart supervisor to activate the changes.
|
||||
|
||||
```eval_rst
|
||||
.. hint::
|
||||
The optimal number of concurrent workers will be different for every system and we recommend experimenting with different figures to find the optimal for your system. Note, that the example of 10 threads is conservative and should work even with smaller systems.
|
||||
|
||||
@@ -37,7 +37,7 @@ dependencies = [
|
||||
"celery>=5.2.0,<6",
|
||||
"django-bootstrap-form",
|
||||
"django-celery-beat>=2.3.0",
|
||||
"django-esi>=4.0.1",
|
||||
"django-esi>=5.0.0",
|
||||
"django-redis>=5.2.0",
|
||||
"django-registration>=3.3,<3.4",
|
||||
"django-sortedm2m",
|
||||
|
||||
13
thirdparty/Supervisor/auth-mumble.conf
vendored
13
thirdparty/Supervisor/auth-mumble.conf
vendored
@@ -1,13 +0,0 @@
|
||||
[program:auth-mumble]
|
||||
command=python authenticator.py
|
||||
directory=/home/allianceserver/allianceauth/thirdparty/Mumble
|
||||
user=allianceserver
|
||||
numprocs=1
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/authenticator.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/authenticator.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopwaitsecs = 600
|
||||
killasgroup=true
|
||||
priority=500
|
||||
28
thirdparty/Supervisor/auth.conf
vendored
28
thirdparty/Supervisor/auth.conf
vendored
@@ -1,28 +0,0 @@
|
||||
[program:celerybeat]
|
||||
command=celery -A alliance_auth beat
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
priority=998
|
||||
|
||||
[program:celeryd]
|
||||
command=celery -A alliance_auth worker
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
numprocs=1
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopwaitsecs = 600
|
||||
killasgroup=true
|
||||
priority=998
|
||||
|
||||
[group:auth]
|
||||
programs=celerybeat,celeryd
|
||||
priority=999
|
||||
Reference in New Issue
Block a user