Merge branch 'v4.x' of gitlab.com:allianceauth/allianceauth into v4docs

This commit is contained in:
Ariel Rin 2024-02-23 22:26:38 +10:00
commit 445683c3d5
No known key found for this signature in database
173 changed files with 5993 additions and 2913 deletions

View File

@ -19,5 +19,6 @@ exclude_lines =
if __name__ == .__main__.:
def __repr__
raise AssertionError
if TYPE_CHECKING:
ignore_errors = True

View File

@ -26,11 +26,11 @@ pre-commit-check:
<<: *only-default
stage: pre-commit
image: python:3.11-bullseye
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
paths:
- ${PRE_COMMIT_HOME}
# variables:
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
# cache:
# paths:
# - ${PRE_COMMIT_HOME}
script:
- pip install pre-commit
- pre-commit run --all-files

View File

@ -12,6 +12,9 @@ build:
- redis
tools:
python: "3.11"
jobs:
post_system_dependencies:
- redis-server --daemonize yes
# Build documentation in the docs/ directory with Sphinx
sphinx:

View File

@ -5,7 +5,7 @@ manage online service access.
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '4.0.0a4'
__version__ = '4.0.0b1'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'

View File

@ -1,6 +1,6 @@
from allianceauth.hooks import DashboardItemHook
from allianceauth import hooks
from .views import dashboard_characters, dashboard_groups, dashboard_admin
from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin
class UserCharactersHook(DashboardItemHook):
@ -26,6 +26,15 @@ class AdminHook(DashboardItemHook):
DashboardItemHook.__init__(
self,
dashboard_admin,
1
)
class ESICheckHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_esi_check,
0
)
@ -43,3 +52,8 @@ def register_groups_hook():
@hooks.register('dashboard_hook')
def register_admin_hook():
return AdminHook()
@hooks.register('dashboard_hook')
def register_esi_hook():
return ESICheckHook()

View File

@ -0,0 +1,12 @@
from django.utils.translation import gettext_lazy as _
# Overide ESI messages in the dashboard widget
# when the returned messages are not helpful or out of date
ESI_ERROR_MESSAGE_OVERRIDES = {
420: _("This software has exceeded the error limit for ESI. "
"If you are a user, please contact the maintainer of this software."
" If you are a developer/maintainer, please make a greater "
"effort in the future to receive valid responses. For tips on how, "
"come have a chat with us in ##3rd-party-dev-and-esi on the EVE "
"Online Discord. https://www.eveonline.com/discord")
}

View File

@ -0,0 +1,10 @@
{% extends 'allianceauth/base.html' %}
{% block page_title %}Dashboard{% endblock page_title %}
{% block content %}
<div>
<h1>Dashboard Dummy</h1>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% load i18n %}
<div class="col-12 col-xl-8 align-self-stretch p-2 ps-0 pe-0 ps-xl-0 pe-xl-2">
<div id="aa-dashboard-panel-characters" class="col-12 col-xl-8 align-self-stretch p-2 ps-0 pe-0 ps-xl-0 pe-xl-2">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">

View File

@ -1,5 +1,5 @@
{% load i18n %}
<div class="col-12 col-xl-4 align-self-stretch py-2 ps-xl-2">
<div id="aa-dashboard-panel-membership" class="col-12 col-xl-4 align-self-stretch py-2 ps-xl-2">
<div class="card h-100">
<div class="card-body">
<h4 class="card-title text-center">{% translate "Membership" %}</h4>

View File

@ -16,7 +16,7 @@
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
</p>
<table class="table" id="table_tokens" style="width: 100%;">
<table class="table w-100" id="table_tokens">
<thead>
<tr>
<th>{% translate "Scopes" %}</th>

View File

@ -4,7 +4,7 @@
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
<select class="form-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 %}

View File

@ -1,6 +1,6 @@
{% extends 'public/base.html' %}
{% load bootstrap %}
{% load django_bootstrap5 %}
{% load i18n %}
{% block page_title %}{% translate "Registration" %}{% endblock %}
@ -12,16 +12,20 @@
{% endblock %}
{% block content %}
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button>
</form>
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card card-login border-secondary p-3">
<div class="card-body">
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-primary btn-block" type="submit">{% translate "Register" %}</button>
</form>
{% include 'public/lang_select.html' %}
</div>
</div>
</div>
{% include 'public/lang_select.html' %}
</div>
{% endblock %}

View File

@ -1,10 +1,12 @@
import json
import requests_mock
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from allianceauth.authentication.views import task_counts
from allianceauth.authentication.views import task_counts, esi_check
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
MODULE_PATH = "allianceauth.authentication.views"
@ -21,6 +23,8 @@ class TestRunningTasksCount(TestCase):
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
def test_should_return_data(
self, mock_active_tasks_count, mock_queued_tasks_count
@ -35,5 +39,164 @@ class TestRunningTasksCount(TestCase):
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
jsonresponse_to_dict(response), {
"tasks_running": 2, "tasks_queued": 3}
)
def test_su_only(
self, mock_active_tasks_count, mock_queued_tasks_count
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# 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, 302)
class TestEsiCheck(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
@requests_mock.Mocker()
def test_401_data_returns_200(
self, m
):
error_json = {
"error": "You have been banned from using ESI. Please contact Technical Support. (support@eveonline.com)"
}
status_code = 401
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_504_data_returns_200(
self, m
):
error_json = {
"error": "Gateway timeout message",
"timeout": 5000
}
status_code = 504
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": error_json
}
)
@requests_mock.Mocker()
def test_420_data_override(
self, m
):
error_json = {
"error": "message from CCP",
}
status_code = 420
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(error_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertNotEqual(
jsonresponse_to_dict(response)["data"],
error_json
)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": {
"error": ESI_ERROR_MESSAGE_OVERRIDES.get(status_code)
}
}
)
@requests_mock.Mocker()
def test_200_data_returns_200(
self, m
):
good_json = {
"players": 5,
"server_version": "69420",
"start_time": "2030-01-01T23:59:59Z"
}
status_code = 200
m.get(
"https://esi.evetech.net/latest/status/?datasource=tranquility",
text=json.dumps(good_json),
status_code=status_code
)
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {
"status": status_code,
"data": good_json
}
)
def test_su_only(
self,
):
self.user.is_superuser = False
self.user.save()
self.user.refresh_from_db()
# given
request = self.factory.get("/")
request.user = self.user
# when
response = esi_check(request)
# then
self.assertEqual(response.status_code, 302)

View File

@ -38,5 +38,7 @@ urlpatterns = [
name='token_refresh'
),
path('dashboard/', views.dashboard, name='dashboard'),
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
path('task-counts/', views.task_counts, name='task_counts'),
path('esi-check/', views.esi_check, name='esi_check'),
]

View File

@ -1,6 +1,6 @@
import logging
from allianceauth.hooks import get_hooks
import requests
from django_registration.backends.activation.views import (
REGISTRATION_SALT, ActivationView as BaseActivationView,
RegistrationView as BaseRegistrationView,
@ -10,7 +10,7 @@ from django_registration.signals import user_registered
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.core import signing
from django.http import JsonResponse
@ -23,14 +23,16 @@ from esi.decorators import token_required
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
from allianceauth.hooks import get_hooks
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
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
from allianceauth.eveonline.autogroups.models import *
from allianceauth.eveonline.autogroups.models import * # noqa: F401, F403
else:
_has_auto_groups = False
@ -54,7 +56,7 @@ def dashboard_groups(request):
context = {
'groups': groups,
}
return render_to_string('authentication/dashboard.groups.html', context=context, request=request)
return render_to_string('authentication/dashboard_groups.html', context=context, request=request)
def dashboard_characters(request):
@ -66,7 +68,7 @@ def dashboard_characters(request):
context = {
'characters': characters
}
return render_to_string('authentication/dashboard.characters.html', context=context, request=request)
return render_to_string('authentication/dashboard_characters.html', context=context, request=request)
def dashboard_admin(request):
@ -76,6 +78,13 @@ def dashboard_admin(request):
return ""
def dashboard_esi_check(request):
if request.user.is_superuser:
return render_to_string('allianceauth/admin-status/esi_check.html', request=request)
else:
return ""
@login_required
def dashboard(request):
_dash_items = list()
@ -135,23 +144,30 @@ def token_refresh(request, token_id=None):
@login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token):
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}")
logger.debug(
f"main_character_change called by user {request.user} for character {token.character_name}")
try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
co = CharacterOwnership.objects.get(
character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist:
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token)
else:
messages.error(
request,
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
_('Cannot change main character to %(char)s: character owned by a different account.') % (
{'char': token.character_name})
)
co = None
if co:
request.user.profile.main_character = co.character
request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
messages.success(request, _('Changed main character to %s') % co.character)
logger.info(
'Changed user {user} main character to {char}'.format(
user=request.user, char=co.character
)
)
return redirect("authentication:dashboard")
@ -159,9 +175,11 @@ 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})))
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % (
{'name': token.character_name})))
return redirect('authentication:dashboard')
@ -204,8 +222,10 @@ def sso_login(request, token):
token.delete()
messages.error(
request,
_('Unable to authenticate as the selected character. '
'Please log in with the main character associated with this account.')
_(
'Unable to authenticate as the selected character. '
'Please log in with the main character associated with this account.'
)
)
return redirect(settings.LOGIN_URL)
@ -278,7 +298,8 @@ class RegistrationView(BaseRegistrationView):
return super().dispatch(request, *args, **kwargs)
def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid'))
user = User.objects.get(
pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request)
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
@ -295,7 +316,8 @@ class RegistrationView(BaseRegistrationView):
def get_email_context(self, activation_key):
context = super().get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
context['url'] = context['site'].domain + \
reverse('registration_activate', args=[activation_key])
return context
@ -328,20 +350,24 @@ class ActivationView(BaseActivationView):
def registration_complete(request):
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.'))
messages.success(request, _(
'Sent confirmation email. Please follow the link to confirm your email address.'))
return redirect('authentication:login')
def activation_complete(request):
messages.success(request, _('Confirmed your email address. Please login to continue.'))
messages.success(request, _(
'Confirmed your email address. Please login to continue.'))
return redirect('authentication:dashboard')
def registration_closed(request):
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
messages.error(request, _(
'Registration of new accounts is not allowed at this time.'))
return redirect('authentication:login')
@user_passes_test(lambda u: u.is_superuser)
def task_counts(request) -> JsonResponse:
"""Return task counts as JSON for an AJAX call."""
data = {
@ -349,3 +375,31 @@ def task_counts(request) -> JsonResponse:
"tasks_queued": queued_tasks_count()
}
return JsonResponse(data)
def check_for_override_esi_error_message(response):
if response.status_code in ESI_ERROR_MESSAGE_OVERRIDES:
return {"error": ESI_ERROR_MESSAGE_OVERRIDES.get(response.status_code)}
else:
return response.json()
@user_passes_test(lambda u: u.is_superuser)
def esi_check(request) -> JsonResponse:
"""Return if ESI ok With error messages and codes as JSON"""
_r = requests.get("https://esi.evetech.net/latest/status/?datasource=tranquility")
data = {
"status": _r.status_code,
"data": check_for_override_esi_error_message(_r)
}
return JsonResponse(data)
@login_required
def dashboard_bs3(request):
"""Render dashboard view with BS3 theme.
This is an internal view used for testing BS3 backward compatibility in AA4 only.
"""
return render(request, 'authentication/dashboard_bs3.html')

View File

@ -31,7 +31,7 @@
<div class="card card-default mt-4">
<div class="card-header clearfix" role="tablist">
<ul class="nav nav-pills text-right float-start">
<ul class="nav nav-pills float-start">
<li class="nav-item" role="presentation">
<a
class="nav-link active"

View File

@ -0,0 +1,57 @@
"""
Alliance Auth Evecharacter API
"""
from typing import Optional
from django.contrib.auth.models import User
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.eveonline.models import EveCharacter
from allianceauth.framework.api.user import get_sentinel_user
def get_main_character_from_evecharacter(
character: EveCharacter,
) -> Optional[EveCharacter]:
"""
Get the main character for a given EveCharacter or None when no main character is set
:param character:
:type character:
:return:
:rtype:
"""
try:
userprofile = character.character_ownership.user.profile
except (
AttributeError,
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
CharacterOwnership.user.RelatedObjectDoesNotExist,
):
return None
return userprofile.main_character
def get_user_from_evecharacter(character: EveCharacter) -> User:
"""
Get the user for an EveCharacter or the sentinel user when no user is found
:param character:
:type character:
:return:
:rtype:
"""
try:
userprofile = character.character_ownership.user.profile
except (
AttributeError,
EveCharacter.character_ownership.RelatedObjectDoesNotExist,
CharacterOwnership.user.RelatedObjectDoesNotExist,
):
return get_sentinel_user()
return userprofile.user

View File

@ -6,17 +6,33 @@ from typing import Optional
from django.contrib.auth.models import User
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.eveonline.models import EveCharacter
def get_sentinel_user() -> User:
def get_all_characters_from_user(user: User) -> list:
"""
Get the sentinel user or create one
Get all characters from a user or an empty list
when no characters are found for the user or the user is None
:param user:
:type user:
:return:
:rtype:
"""
return User.objects.get_or_create(username="deleted")[0]
if user is None:
return []
try:
characters = [
char.character for char in CharacterOwnership.objects.filter(user=user)
]
except AttributeError:
return []
return characters
def get_main_character_from_user(user: User) -> Optional[EveCharacter]:
"""
@ -62,3 +78,13 @@ def get_main_character_name_from_user(user: User) -> str:
return str(user)
return username
def get_sentinel_user() -> User:
"""
Get the sentinel user or create one
:return:
"""
return User.objects.get_or_create(username="deleted")[0]

View File

@ -13,6 +13,45 @@
}
}
/* Side Navigation
------------------------------------------------------------------------------------- */
@media all {
#sidebar > div {
width: 325px;
}
/* Menu items in general */
#sidebar-menu li > a,
#sidebar-menu li > ul > li > a {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 210px;
}
/* Parent items with chevron and possible badge */
#sidebar-menu li:has(span.badge) > a[data-bs-toggle="collapse"] {
max-width: 180px;
}
/* Child items with possible badge */
#sidebar-menu li > ul > li > a {
max-width: 189px;
}
/* Chevron icons */
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-down,
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-right {
display: block;
width: 16px;
}
#sidebar-menu [data-bs-toggle="collapse"] > i.fa-chevron-right,
#sidebar-menu [data-bs-toggle="collapse"].collapsed > i.fa-chevron-down {
display: none;
}
}
/* Cursor classes
------------------------------------------------------------------------------------- */
@media all {
@ -72,9 +111,16 @@
border: 1px solid var(--bs-border-color);
border-left-width: 0.25rem;
border-radius: 0.25rem;
margin-bottom: 1.25rem;
margin-top: 1.25rem;
padding: 1.25rem;
margin-bottom: 1rem;
padding: 1rem;
}
.aa-callout.aa-callout-sm {
padding: 0.5rem;
}
.aa-callout.aa-callout-lg {
padding: 1.5rem;
}
/* Last item bottom margin should be 0 */

View File

@ -71,7 +71,7 @@
{% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %}
{% include 'bundles/moment-js.html' with locale=True %}
{# {% include 'bundles/filterdropdown-js.html' %}#}
{% include 'bundles/filterdropdown-js.html' %}
<script>
$.fn.dataTable.moment = (format, locale) => {
@ -117,7 +117,8 @@
idx: 6
}
],
bootstrap: true
bootstrap: true,
bootstrap_version: 5
},
"stateSave": true,
"stateDuration": 0

View File

@ -30,7 +30,11 @@
<tr>
<th>{% translate "Name" %}</th>
<th>{% translate "Description" %}</th>
<th>{% translate "Leaders" %}<span class="m-1 fw-lighter badge bg-primary">{% translate "User" %}</span><span class="m-1 fw-lighter badge bg-secondary ">{% translate "Group" %}</span></th>
<th>
{% translate "Leaders" %}<br>
<span class="my-1 me-1 fw-lighter badge bg-primary">{% translate "User" %}</span>
<span class="my-1 me-1 fw-lighter badge bg-secondary">{% translate "Group" %}</span>
</th>
<th></th>
</tr>
</thead>
@ -39,13 +43,23 @@
{% for g in groups %}
<tr>
<td>{{ g.group.name }}</td>
<td>{{ g.group.authgroup.description|linebreaks|urlize }}</td>
<td>
{% if g.group.authgroup.description %}
{{ g.group.authgroup.description|linebreaks|urlize }}
{% endif %}
</td>
<td style="max-width: 30%;">
{% if g.group.authgroup.group_leaders.all.count %}
{% for leader in g.group.authgroup.group_leaders.all %}{% if leader.profile.main_character %}<span class="m-1 badge bg-primary">{{leader.profile.main_character}}</span>{% endif %}{% endfor %}
{% for leader in g.group.authgroup.group_leaders.all %}
{% if leader.profile.main_character %}
<span class="my-1 me-1 badge bg-primary">{{leader.profile.main_character}}</span>
{% endif %}
{% endfor %}
{% endif %}
{% if g.group.authgroup.group_leaders.all.count %}
{% for group in g.group.authgroup.group_leader_groups.all %}<span class="badge bg-secondary">{{group.name}}</span>{% endfor %}
{% for group in g.group.authgroup.group_leader_groups.all %}
<span class="my-1 me-1 badge bg-secondary">{{group.name}}</span>
{% endfor %}
{% endif %}
</td>
<td class="text-end">

View File

@ -13,8 +13,8 @@ msgid ""
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: 2023-10-08 09:23+0000\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+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"
@ -31,7 +31,7 @@ msgstr "Google Analytics Universal"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Zur Ausführung dieser Aktion ist ein Hauptcharakter erforderlich. Füge unten"
@ -47,63 +47,68 @@ msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr ""
"Du kannst diese eingeschränkten Gruppen nicht hinzufügen oder entfernen: %s"
#: allianceauth/authentication/models.py:80
#: allianceauth/authentication/models.py:71
msgid "English"
msgstr "Englisch"
#: allianceauth/authentication/models.py:81
#: allianceauth/authentication/models.py:72
msgid "German"
msgstr "Deutsch"
#: allianceauth/authentication/models.py:82
#: allianceauth/authentication/models.py:73
msgid "Spanish"
msgstr "Spanisch"
#: allianceauth/authentication/models.py:83
#: allianceauth/authentication/models.py:74
msgid "Chinese Simplified"
msgstr "Chinesisch vereinfacht"
#: allianceauth/authentication/models.py:84
#: allianceauth/authentication/models.py:75
msgid "Russian"
msgstr "Russisch"
#: allianceauth/authentication/models.py:85
#: allianceauth/authentication/models.py:76
msgid "Korean"
msgstr "Koreanisch"
#: allianceauth/authentication/models.py:86
#: allianceauth/authentication/models.py:77
msgid "French"
msgstr "Französisch"
#: allianceauth/authentication/models.py:87
#: allianceauth/authentication/models.py:78
msgid "Japanese"
msgstr "Japanisch"
#: allianceauth/authentication/models.py:88
#: allianceauth/authentication/models.py:79
msgid "Italian"
msgstr "Italienisch"
#: allianceauth/authentication/models.py:91
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr "Ukrainisch"
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Sprache"
#: allianceauth/authentication/models.py:96
#: allianceauth/authentication/models.py:101
#: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode"
msgstr "Nachtmodus"
#: allianceauth/authentication/models.py:110
#: allianceauth/authentication/models.py:115
#, python-format
msgid "State changed to: %s"
msgstr "Status geändert zu %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Dein Nutzerstatus ist nun %(state)s"
#: 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 "Dashboard"
@ -161,8 +166,54 @@ msgstr "Corp"
msgid "Alliance"
msgstr "Allianz"
#: allianceauth/authentication/templates/authentication/tokens.html:7
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62
msgid "Token Management"
msgstr "Token-Verwaltung"
#: allianceauth/authentication/templates/authentication/tokens.html:12
msgid "Scopes"
msgstr "Geltungsbereiche"
#: 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 "Aktionen"
#: 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 "Charakter"
#: 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 ""
"Diese Seite ist der beste Versuch, aber Sicherungen oder Datenbankprotokolle"
" können weiterhin Deine Token enthalten. Widerrufe die Token nach "
"Möglichkeit immer auf https://community.eveonline.com/support/third-party-"
"applications/."
#: 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 "Einloggen"
@ -195,7 +246,7 @@ msgstr "Registrieren"
msgid "Invalid or expired activation link."
msgstr "Ungültiger oder abgelaufener Aktivierungslink."
#: 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 "
@ -204,32 +255,34 @@ msgstr ""
"Der Haputcharakter kann nicht zu %(char)s geändert werden. Dieser Charakter "
"gehört zu einem anderen Konto."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Haupcharakter zu %(char)s geändert"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "%(name)s zu Deinem Konto hinzugefügt."
#: 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 ""
"Es ist nicht möglich %(name)s zu Deinem Konto hinzu zu fügen: Dieser hat "
"bereits ein eigenes Konto."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Authentifizierung mit dem ausgewählten Charakter nicht möglich."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Token zur Registrierung ist abgelaufen."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
@ -237,11 +290,11 @@ msgstr ""
"Bestätigungs-E-Mail gesendet. Bitte folge dem Link, um Deine E-Mail-Adresse "
"zu bestätigen."
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr "Deine E-Mail Adresse wurde bestätigt. Bitte einloggen zum Fortfahren."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "Registrierung von neuen Konten ist zur Zeit nicht erlaubt."
@ -284,19 +337,6 @@ msgstr "Nicht registriert"
msgid "Last update:"
msgstr "Letzte Aktualisierung:"
#: 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 "Charakter"
#: allianceauth/corputils/templates/corputils/corpstats.html:75
#: allianceauth/corputils/templates/corputils/search.html:14
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
@ -630,20 +670,25 @@ msgstr ""
msgid "Group Management"
msgstr "Gruppenverwaltung"
#: allianceauth/groupmanagement/forms.py:15
#: allianceauth/groupmanagement/forms.py:18
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Nutzer"
#: allianceauth/groupmanagement/forms.py:52
msgid "This name has been reserved and can not be used for groups."
msgstr ""
"Dieser Name ist reserviert und kann nicht als Gruppenname genutzt werden."
#: allianceauth/groupmanagement/forms.py:25
#: allianceauth/groupmanagement/forms.py:62
msgid "(auto)"
msgstr "(automatisch)"
#: allianceauth/groupmanagement/forms.py:34
#: allianceauth/groupmanagement/forms.py:71
msgid "There already exists a group with that name."
msgstr "Es existiert bereits eine Gruppe mit diesem Namen."
#: 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_* "
@ -653,13 +698,13 @@ msgstr ""
"<br>Dies ist für Gruppen genutzt wie Mitglieder, Corp_*, Allianz_*, "
"etc.<br><b>Überschreibt die Versteckt und Offen Option wenn gesetzt</b>"
#: 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 ""
"Diese Gruppe ist nicht sichtbar, aber Nutzer können dennoch beitreten wenn "
"diese den Link hierzu haben."
#: 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."
@ -668,7 +713,7 @@ msgstr ""
"Anfrage.<br>Wenn die Gruppe nicht offen ist, müssen Anfragen manuell "
"bestätigt werden."
#: 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 "
@ -680,7 +725,7 @@ msgstr ""
" wird Nutzer nicht von dieser Gruppe entfernen wenn diese nicht länger "
"authentifiziert sind."
#: 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."
@ -688,7 +733,7 @@ msgstr ""
"Gruppe ist eingeschränkt. Das bedeutet, dass zum Hinzufügen oder Entfernen "
"von Benutzern für diese Gruppe ein Superuser/Administrator erforderlich ist."
#: 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 "
@ -698,7 +743,7 @@ msgstr ""
"<code>auth.group_management</code> Berechtigung um Nutzern zu erlauben alle "
"Gruppen zu verwalten<br>"
#: 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 "
@ -708,7 +753,7 @@ msgstr ""
"Nutze die <code>auth.group_management</code> Berechtigung um Nutzern zu "
"erlauben alle Gruppen zu verwalten.<br>"
#: 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>"
@ -716,42 +761,42 @@ msgstr ""
"Die hier aufgeführten Status können dieser Gruppe beitreten, sofern sie über"
" die entsprechenden Berechtigungen verfügen.<br>"
#: 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 ""
"Kurze Beschreibung <i>(max. 512 Zeichen)</i> der Gruppe die dem Nutzer "
"angezeigt wird."
#: allianceauth/groupmanagement/models.py:178
#: allianceauth/groupmanagement/models.py:177
msgid "Can request non-public groups"
msgstr "Kann nicht öffentlichen Gruppen beitreten"
#: allianceauth/groupmanagement/models.py:209
#: allianceauth/groupmanagement/models.py:208
msgid "name"
msgstr "Name"
#: allianceauth/groupmanagement/models.py:212
#: allianceauth/groupmanagement/models.py:211
msgid "Name that can not be used for groups."
msgstr "Name kann nicht für Gruppen genutzt werden"
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "reason"
msgstr "Grund"
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "Reason why this name is reserved."
msgstr "Grund wieso dieser Name reserviert ist."
#: allianceauth/groupmanagement/models.py:218
#: allianceauth/groupmanagement/models.py:217
msgid "created by"
msgstr "Erstellt bei"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "created at"
msgstr "Erstellt"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "Date when this entry was created"
msgstr "Datum der Erstellung dieses Eintrags"
@ -978,26 +1023,26 @@ msgstr "Gruppenanfragen"
msgid "Group Membership"
msgstr "Gruppenmitgliedschaft"
#: allianceauth/groupmanagement/views.py:163
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "Benutzer %(user)s von Gruppe %(group)s entfernt."
#: allianceauth/groupmanagement/views.py:165
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "Benutzer existiert nicht in dieser Gruppe"
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "Gruppe existiert nicht"
#: allianceauth/groupmanagement/views.py:195
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Beitrittsanfrage von %(mainchar)s zur Gruppe %(group)s akzeptiert."
#: 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 "
@ -1006,18 +1051,18 @@ msgstr ""
"Bei der Bearbeitung des Beitrittsanfrage von %(mainchar)s zur Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Beitrittsanfrage von %(mainchar)s zur Gruppe %(group)s abgelehnt."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsanfrage von %(mainchar)s für Gruppe %(group)s akzeptiert."
#: 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 "
@ -1026,42 +1071,42 @@ msgstr ""
"Bei der Bearbeitung des Austrittsanfrage von %(mainchar)s für Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsanfrage von %(mainchar)s für Gruppe %(group)s abgelehnt."
#: 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 "Du kannst dieser Gruppe nicht beitreten"
#: allianceauth/groupmanagement/views.py:341
#: allianceauth/groupmanagement/views.py:344
msgid "You are already a member of that group."
msgstr "Du bist bereits Mitglied dieser Gruppe."
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:361
msgid "You already have a pending application for that group."
msgstr "Du hast bereits für diese Gruppe angefragt."
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:370
#, python-format
msgid "Applied to group %(group)s."
msgstr "Beitritt zur Gruppe %(group)s beantragt."
#: allianceauth/groupmanagement/views.py:377
#: allianceauth/groupmanagement/views.py:380
msgid "You cannot leave that group"
msgstr "Du kannst diese Gruppe nicht verlassen"
#: allianceauth/groupmanagement/views.py:381
#: allianceauth/groupmanagement/views.py:384
msgid "You are not a member of that group"
msgstr "Du bist kein Mitglied dieser Gruppe"
#: allianceauth/groupmanagement/views.py:393
#: allianceauth/groupmanagement/views.py:396
msgid "You already have a pending leave request for that group."
msgstr "Du hast bereits eine ausstehendes Austrittsanfrage für diese Gruppe."
#: allianceauth/groupmanagement/views.py:409
#: allianceauth/groupmanagement/views.py:412
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Austrittsanfrage für Gruppe %(group)s gesendet."
@ -1123,16 +1168,6 @@ msgstr "Erstelle Bewerbung"
msgid "Username"
msgstr "Benutzername"
#: 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 "Aktionen"
#: allianceauth/hrapplications/templates/hrapplications/management.html:38
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
#: allianceauth/hrapplications/templates/hrapplications/management.html:143
@ -1471,10 +1506,6 @@ msgstr "Model"
msgid "Code Name"
msgstr "Code Name"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Nutzer"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:41
msgid "States"
msgstr "Status"
@ -2218,14 +2249,12 @@ msgstr ""
"Status von %(total)s verarbeiteten Tasks • innerhalb der letzten %(latest)s"
#: allianceauth/templates/allianceauth/admin-status/overview.html:95
#, python-format
msgid ""
"\n"
" %(queue_length)s queued tasks\n"
" "
msgstr ""
"\n"
"%(queue_length)s Tasks in der Warteschlange"
msgid "running"
msgstr "laufend"
#: allianceauth/templates/allianceauth/admin-status/overview.html:96
msgid "queued"
msgstr "eingereiht"
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2240,11 +2269,11 @@ msgid "AA Support Discord"
msgstr "AA Support Discord"
#: 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 "Nutzermenü"
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:56
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:67
msgid "Logout"
msgstr "Ausloggen"
@ -2300,22 +2329,30 @@ msgid "Objective"
msgstr "Ziel"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr "Absoluter Timer"
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr "Datum und Uhrzeit"
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Tage verbleibend"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Stunden verbleibend"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Minuten verbleibend"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Wichtig"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Auf Corp beschränkt"

File diff suppressed because it is too large Load Diff

View File

@ -6,18 +6,18 @@
# Translators:
# Fegpawn Kaundur, 2023
# frank1210 <francolopez_16@hotmail.com>, 2023
# trenus, 2023
# Joel Falknau <ozirascal@gmail.com>, 2023
# Young Anexo, 2023
# Joel Falknau <ozirascal@gmail.com>, 2023
# trenus, 2023
#
#, fuzzy
msgid ""
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: 2023-10-08 09:23+0000\n"
"Last-Translator: Young Anexo, 2023\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+0000\n"
"Last-Translator: trenus, 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"
@ -33,7 +33,7 @@ msgstr "Google Analytics Universal"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Se necesita un personaje principal para realizar esa acción. Añade uno a "
@ -48,63 +48,68 @@ msgstr "E-mail"
msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "No puedes añadir o eliminar estos grupos restringidos: %s"
#: allianceauth/authentication/models.py:80
#: allianceauth/authentication/models.py:71
msgid "English"
msgstr "Inglés"
#: allianceauth/authentication/models.py:81
#: allianceauth/authentication/models.py:72
msgid "German"
msgstr "Alemán"
#: allianceauth/authentication/models.py:82
#: allianceauth/authentication/models.py:73
msgid "Spanish"
msgstr "Español"
#: allianceauth/authentication/models.py:83
#: allianceauth/authentication/models.py:74
msgid "Chinese Simplified"
msgstr "Chino Simplificado"
#: allianceauth/authentication/models.py:84
#: allianceauth/authentication/models.py:75
msgid "Russian"
msgstr "Ruso"
#: allianceauth/authentication/models.py:85
#: allianceauth/authentication/models.py:76
msgid "Korean"
msgstr "Coreano"
#: allianceauth/authentication/models.py:86
#: allianceauth/authentication/models.py:77
msgid "French"
msgstr "Francés"
#: allianceauth/authentication/models.py:87
#: allianceauth/authentication/models.py:78
msgid "Japanese"
msgstr "Japonés"
#: allianceauth/authentication/models.py:88
#: allianceauth/authentication/models.py:79
msgid "Italian"
msgstr "Italiano"
#: allianceauth/authentication/models.py:91
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Idioma"
#: allianceauth/authentication/models.py:96
#: allianceauth/authentication/models.py:101
#: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode"
msgstr "Modo Nocturno"
#: allianceauth/authentication/models.py:110
#: allianceauth/authentication/models.py:115
#, python-format
msgid "State changed to: %s"
msgstr "Estado cambiado a: %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "El estado de su usuario es ahora: %(state)s"
#: 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 "Página principal"
@ -162,8 +167,50 @@ msgstr "Corporación"
msgid "Alliance"
msgstr "Allianza"
#: 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 "Acciones"
#: 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 "Personaje"
#: 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 "Ingresar"
@ -197,7 +244,7 @@ msgstr "Registrar"
msgid "Invalid or expired activation link."
msgstr "Enlace de activacion expirado o invalido"
#: 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 "
@ -206,45 +253,47 @@ msgstr ""
"No se puede cambiar de personaje principal a %(char)s: personaje "
"perteneciente a otra cuenta."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Se ha cambiado tu personaje principal ha %(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "Se ha agregado a %(name)s a tu cuenta"
#: 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 ""
"Se fallo en agregar a %(name)s a tu cuenta: Ya se encuentra registrado en "
"otra cuenta."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Imposible validar con el personaje seleccionado."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "El token de registracion expiro."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr ""
"Confirmacion de mail enviada. Por favor siga el enlace para confirmar "
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Se ha confirmado su direccion de mail. Por favor igrese su token para "
"continuar."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "En este momento no se permite el registro de nuevas cuentas."
@ -287,19 +336,6 @@ msgstr "Sin registro"
msgid "Last update:"
msgstr "Ultima Actualizacion:"
#: 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 "Personaje"
#: allianceauth/corputils/templates/corputils/corpstats.html:75
#: allianceauth/corputils/templates/corputils/search.html:14
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
@ -636,19 +672,24 @@ msgstr ""
msgid "Group Management"
msgstr "Manejo de Grupo"
#: allianceauth/groupmanagement/forms.py:15
#: allianceauth/groupmanagement/forms.py:18
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Usuarios"
#: allianceauth/groupmanagement/forms.py:52
msgid "This name has been reserved and can not be used for groups."
msgstr "Este nombre ha sido reservado y no puede utilizarse para grupos."
#: allianceauth/groupmanagement/forms.py:25
#: allianceauth/groupmanagement/forms.py:62
msgid "(auto)"
msgstr "(auto)"
#: allianceauth/groupmanagement/forms.py:34
#: allianceauth/groupmanagement/forms.py:71
msgid "There already exists a group with that name."
msgstr "Ya existe un grupo con ese nombre."
#: 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_* "
@ -658,13 +699,13 @@ msgstr ""
"grupo.<br>Se utiliza para grupos como Miembros, Corp_*, Alliance_*, "
"etc.<br><b>Anula las opciones Oculto y Abierto cuando se selecciona.</b>"
#: 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 ""
"El grupo está oculto para los usuarios, pero aún pueden unirse con el enlace"
" correcto."
#: 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."
@ -673,7 +714,7 @@ msgstr ""
"soliciten.<br>Si el grupo no está abierto, los usuarios necesitarán que su "
"solicitud sea aprobada manualmente."
#: 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 "
@ -685,7 +726,7 @@ msgstr ""
"grupo.<br>Auth no eliminará automáticamente a los usuarios de este grupo "
"cuando ya no estén autenticados."
#: 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."
@ -693,7 +734,7 @@ msgstr ""
"El grupo está restringido. Esto significa que para añadir o eliminar "
"usuarios de este grupo se requiere un superusuario administrador."
#: 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 "
@ -703,7 +744,7 @@ msgstr ""
" permiso <code>auth.group_management</code> para permitir que un usuario "
"gestione todos los grupos.<br>"
#: 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 "
@ -713,7 +754,7 @@ msgstr ""
"grupo. Utilice el <code>permiso auth.group_management</code> para permitir "
"que un usuario gestione todos los grupos.<br>"
#: 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>"
@ -721,42 +762,42 @@ msgstr ""
"Los estados que figuren en esta lista podrán unirse a este grupo siempre que"
" dispongan de los permisos adecuados.<br>"
#: 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 ""
"Breve descripción <i>(máx. 512 caracteres)</i> del grupo que se muestra a "
"los usuarios."
#: allianceauth/groupmanagement/models.py:178
#: allianceauth/groupmanagement/models.py:177
msgid "Can request non-public groups"
msgstr "Se pueden solicitar grupos no públicos"
#: allianceauth/groupmanagement/models.py:209
#: allianceauth/groupmanagement/models.py:208
msgid "name"
msgstr "nombre"
#: allianceauth/groupmanagement/models.py:212
#: allianceauth/groupmanagement/models.py:211
msgid "Name that can not be used for groups."
msgstr "Nombre que no se puede utilizar para los grupos."
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "reason"
msgstr "razón"
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "Reason why this name is reserved."
msgstr "Razón por la que este nombre está reservado."
#: allianceauth/groupmanagement/models.py:218
#: allianceauth/groupmanagement/models.py:217
msgid "created by"
msgstr "creado por"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "created at"
msgstr "creado en"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "Date when this entry was created"
msgstr "Fecha de creación de esta entrada"
@ -983,26 +1024,26 @@ msgstr "Solicitudes de Grupo"
msgid "Group Membership"
msgstr "Membresia de Grupo"
#: allianceauth/groupmanagement/views.py:163
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "El usuario %(user)s fue removido del grupo %(group)s"
#: allianceauth/groupmanagement/views.py:165
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "El usuario no existe en ese grupos"
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "El grupo no existe"
#: allianceauth/groupmanagement/views.py:195
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Solicitud aceptada de %(mainchar)s a %(group)s"
#: 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 "
@ -1011,18 +1052,18 @@ msgstr ""
"Ocurrio un error cuando se intento procesar la informacion de %(mainchar)s "
"al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Se rechazo la solicitud de %(mainchar)s al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Se acepto la solicitud de %(mainchar)s para dejar el grupo %(group)s."
#: 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 "
@ -1031,43 +1072,43 @@ msgstr ""
"Se ha producido un error al procesar la solicitud de %(mainchar)s para "
"abandonar %(group)s."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr ""
"Se rechazo la solicitud de %(mainchar)s para dejar el grupo %(group)s."
#: 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 "No puedes unirte a ese grupo"
#: allianceauth/groupmanagement/views.py:341
#: allianceauth/groupmanagement/views.py:344
msgid "You are already a member of that group."
msgstr "Ya eres miembro de ese grupo."
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:361
msgid "You already have a pending application for that group."
msgstr "Ya tiene una solicitud pendiente para ese grupo."
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:370
#, python-format
msgid "Applied to group %(group)s."
msgstr "Solicitud enviada al grupo %(group)s."
#: allianceauth/groupmanagement/views.py:377
#: allianceauth/groupmanagement/views.py:380
msgid "You cannot leave that group"
msgstr "No puedes dejar el grupos"
#: allianceauth/groupmanagement/views.py:381
#: allianceauth/groupmanagement/views.py:384
msgid "You are not a member of that group"
msgstr "No eres miembro de ese grupo"
#: allianceauth/groupmanagement/views.py:393
#: allianceauth/groupmanagement/views.py:396
msgid "You already have a pending leave request for that group."
msgstr "Ya tiene una solicitud de baja pendiente para ese grupo."
#: allianceauth/groupmanagement/views.py:409
#: allianceauth/groupmanagement/views.py:412
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Solicitaste dejar el grupo %(group)s."
@ -1129,16 +1170,6 @@ msgstr "Crear Solicitud"
msgid "Username"
msgstr "Usuario"
#: 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 "Acciones"
#: allianceauth/hrapplications/templates/hrapplications/management.html:38
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
#: allianceauth/hrapplications/templates/hrapplications/management.html:143
@ -1477,10 +1508,6 @@ msgstr "Modelo"
msgid "Code Name"
msgstr "Nombre codigo"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Usuarios"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:41
msgid "States"
msgstr "Estados"
@ -2219,14 +2246,12 @@ msgstr ""
"Estado de %(total)s tareas procesadas • últimos %(latest)s"
#: 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 ""
"\n"
"%(queue_length)s tareas en cola"
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2241,11 +2266,11 @@ msgid "AA Support Discord"
msgstr "Soporte Discord AA"
#: 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 "Menú de usuario"
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:56
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:67
msgid "Logout"
msgstr "Salir"
@ -2301,22 +2326,30 @@ msgid "Objective"
msgstr "Objetivo"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Dias restantes"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Horas Restantes"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Minutos Restantes"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Importante"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Restringido a Corp"

View File

@ -4,23 +4,23 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Keven D. <theenarki@gmail.com>, 2023
# rockclodbuster, 2023
# Geoffrey Fabbro, 2023
# Mohssine Daghghar, 2023
# François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2023
# Mickael PATTE, 2023
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2023
# Mickael Gr4vity, 2023
# Idea ., 2023
# rockclodbuster, 2023
# Keven D. <theenarki@gmail.com>, 2023
# Mohssine Daghghar, 2023
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2023
# draktanar KarazGrong <umbre@fallenstarscreations.com>, 2023
# Geoffrey Fabbro, 2023
#
#, fuzzy
msgid ""
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: 2023-10-08 09:23+0000\n"
"Last-Translator: Idea ., 2023\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+0000\n"
"Last-Translator: Geoffrey Fabbro, 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"
@ -36,7 +36,7 @@ msgstr "Google Analytique Universelle"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Un personnage principal est nécessaire pour effectuer cette action. Ajoutez-"
@ -53,63 +53,68 @@ msgstr ""
"Vous n'avez pas lautorisation d'ajouter ou d'enlever ces groupes "
"restreints: %s"
#: allianceauth/authentication/models.py:80
#: allianceauth/authentication/models.py:71
msgid "English"
msgstr "Anglais"
#: allianceauth/authentication/models.py:81
#: allianceauth/authentication/models.py:72
msgid "German"
msgstr "Allemand"
#: allianceauth/authentication/models.py:82
#: allianceauth/authentication/models.py:73
msgid "Spanish"
msgstr "Espagnol"
#: allianceauth/authentication/models.py:83
#: allianceauth/authentication/models.py:74
msgid "Chinese Simplified"
msgstr "Chinois simplifié"
#: allianceauth/authentication/models.py:84
#: allianceauth/authentication/models.py:75
msgid "Russian"
msgstr "Russe"
#: allianceauth/authentication/models.py:85
#: allianceauth/authentication/models.py:76
msgid "Korean"
msgstr "Coréen"
#: allianceauth/authentication/models.py:86
#: allianceauth/authentication/models.py:77
msgid "French"
msgstr "Français"
#: allianceauth/authentication/models.py:87
#: allianceauth/authentication/models.py:78
msgid "Japanese"
msgstr "Japonais"
#: allianceauth/authentication/models.py:88
#: allianceauth/authentication/models.py:79
msgid "Italian"
msgstr "Italien"
#: allianceauth/authentication/models.py:91
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Langue"
#: allianceauth/authentication/models.py:96
#: allianceauth/authentication/models.py:101
#: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode"
msgstr "Mode Nuit"
#: allianceauth/authentication/models.py:110
#: allianceauth/authentication/models.py:115
#, python-format
msgid "State changed to: %s"
msgstr "État changé à: %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "L'état de votre personnage est maintenant: %(state)s"
#: 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 "Écran de bord"
@ -167,8 +172,50 @@ msgstr "Corpo"
msgid "Alliance"
msgstr "Alliance"
#: 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 "Actions"
#: 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 "Personnage"
#: 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 "Connexion"
@ -202,7 +249,7 @@ msgstr "S'inscrire"
msgid "Invalid or expired activation link."
msgstr "Lien d'activation invalide ou expiré."
#: 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 "
@ -211,30 +258,32 @@ msgstr ""
"Impossible de changer le personnage principal à %(char)s. Le personnage "
"appartient à un autre compte."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Changé le personnage principal à %(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "Ajouté %(name)s à votre compte."
#: 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 "Impossible d'ajouter %(name)s à votre compte: ils ont déjà un compte."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Personnage principal : échec de l'identification."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Le token d'enregistrement est expiré."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
@ -242,12 +291,12 @@ msgstr ""
"Email de confirmation envoyé. Cliquez sur le lien pour valider votre adresse"
" email."
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Votre adresse email a été confirmé. Veuillez vous connecter pour continuer."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "La création de nouveaux comptes n'est pas actuellement permise."
@ -290,19 +339,6 @@ msgstr "Pas inscrit"
msgid "Last update:"
msgstr "Dernière mise à jour:"
#: 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 "Personnage"
#: allianceauth/corputils/templates/corputils/corpstats.html:75
#: allianceauth/corputils/templates/corputils/search.html:14
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
@ -639,19 +675,24 @@ msgstr ""
msgid "Group Management"
msgstr "Gestion de groupe"
#: allianceauth/groupmanagement/forms.py:15
#: allianceauth/groupmanagement/forms.py:18
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Utilisateurs"
#: allianceauth/groupmanagement/forms.py:52
msgid "This name has been reserved and can not be used for groups."
msgstr "Ce nom a été réserver et il ne peut être utilisé pour les groupes."
#: allianceauth/groupmanagement/forms.py:25
#: allianceauth/groupmanagement/forms.py:62
msgid "(auto)"
msgstr "(automatique)"
#: allianceauth/groupmanagement/forms.py:34
#: allianceauth/groupmanagement/forms.py:71
msgid "There already exists a group with that name."
msgstr "Il existe déjà un groupe portant ce nom."
#: 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_* "
@ -662,13 +703,13 @@ msgstr ""
"Corporations _*, Alliance etc.<br><b> Annule les options masquer et exposer "
"quand sélectionner."
#: 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 ""
"Le groupe est caché aux utilisateurs, mais ils peuvent toujours rejoindre "
"avec le bon lien."
#: 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."
@ -677,7 +718,7 @@ msgstr ""
" demande. <br> Si le groupe nest pas ouvert, les utilisateurs auront besoin"
" que leurs demandes soit approuvées manuellement."
#: 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 "
@ -689,7 +730,7 @@ msgstr ""
"groupe.<br> L' Auth ne supprimera pas automatiquement les utilisateurs de ce"
" groupe lorsquils ne seront plus authentifiés."
#: 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."
@ -697,62 +738,62 @@ msgstr ""
"Le groupe est restreint. Cela signifie que lajout ou la suppression "
"dutilisateurs pour ce groupe nécessite un administrateur superutilisateur."
#: 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 ""
"Brève description <i> (512 caractères maximum) </i> du groupe présenté aux "
"utilisateurs."
#: allianceauth/groupmanagement/models.py:178
#: allianceauth/groupmanagement/models.py:177
msgid "Can request non-public groups"
msgstr "Peut demander des groupes non publics"
#: allianceauth/groupmanagement/models.py:209
#: allianceauth/groupmanagement/models.py:208
msgid "name"
msgstr "Nom"
#: allianceauth/groupmanagement/models.py:212
#: allianceauth/groupmanagement/models.py:211
msgid "Name that can not be used for groups."
msgstr "Nom qui ne peut pas être utilisé pour les groupes."
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "reason"
msgstr "raison"
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "Reason why this name is reserved."
msgstr "Raison pour laquelle ce nom est réservé."
#: allianceauth/groupmanagement/models.py:218
#: allianceauth/groupmanagement/models.py:217
msgid "created by"
msgstr "créé par"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "created at"
msgstr "créé à"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "Date when this entry was created"
msgstr "Date de création de cette entrée"
@ -979,26 +1020,26 @@ msgstr "Demandes de groupe"
msgid "Group Membership"
msgstr "Groupe appartenance "
#: allianceauth/groupmanagement/views.py:163
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "L'utilisateur %(user)s à été retiré du groupe %(group)s."
#: allianceauth/groupmanagement/views.py:165
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "L'utilisateur n'existe pas dans ce groupe"
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "Groupe non-existant"
#: allianceauth/groupmanagement/views.py:195
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Candidature de %(mainchar)s acceptée à %(group)s."
#: 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 "
@ -1007,18 +1048,18 @@ msgstr ""
"Une erreur inattendue est survenue durant le traitement de l'application de "
"%(mainchar)s à %(group)s."
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "L'application de %(mainchar)s à %(group)s est rejetée."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "La demande de retirer %(mainchar)s de %(group)s est acceptée."
#: 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 "
@ -1027,42 +1068,42 @@ msgstr ""
"Une erreur inattendue est survenue durant le traitement de la demande de "
"retirer %(mainchar)s de %(group)s."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "La remande de retirer %(mainchar)s de %(group)s a été refusée."
#: 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 "Vous ne pouvez pas joindre ce groupe."
#: allianceauth/groupmanagement/views.py:341
#: allianceauth/groupmanagement/views.py:344
msgid "You are already a member of that group."
msgstr "Vous faites déjà parti de ce groupe."
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:361
msgid "You already have a pending application for that group."
msgstr "Vous avez déjà une application en attente pour joindre ce groupe."
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:370
#, python-format
msgid "Applied to group %(group)s."
msgstr "Appliqué au groupe %(group)s."
#: allianceauth/groupmanagement/views.py:377
#: allianceauth/groupmanagement/views.py:380
msgid "You cannot leave that group"
msgstr "Vous ne pouvez pas quitter ce groupe."
#: allianceauth/groupmanagement/views.py:381
#: allianceauth/groupmanagement/views.py:384
msgid "You are not a member of that group"
msgstr "Vous n'êtes pas un membre de ce groupe."
#: allianceauth/groupmanagement/views.py:393
#: allianceauth/groupmanagement/views.py:396
msgid "You already have a pending leave request for that group."
msgstr "Vous avec déjà une demande de quitter ce groupe en attente."
#: allianceauth/groupmanagement/views.py:409
#: allianceauth/groupmanagement/views.py:412
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Appliqué pour quitter le groupe %(group)s."
@ -1124,16 +1165,6 @@ msgstr "Créer une application"
msgid "Username"
msgstr "Nom d'utilisateur"
#: 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 "Actions"
#: allianceauth/hrapplications/templates/hrapplications/management.html:38
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
#: allianceauth/hrapplications/templates/hrapplications/management.html:143
@ -1472,10 +1503,6 @@ msgstr "Modèle"
msgid "Code Name"
msgstr "Nom De Code"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Utilisateurs"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:41
msgid "States"
msgstr "États"
@ -2217,15 +2244,12 @@ 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 ""
"\n"
" %(queue_length)stâches en file d'attente\n"
" "
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2240,11 +2264,11 @@ msgid "AA Support Discord"
msgstr "Support Discord AA"
#: 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 "Menu Utilisateur"
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:56
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:67
msgid "Logout"
msgstr "Déconnexion"
@ -2300,22 +2324,30 @@ msgid "Objective"
msgstr "Objectif"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Jour restants"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Heures restantes"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Minutes restantes"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Important"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Limité à la Corporation"

View File

@ -12,8 +12,8 @@ msgid ""
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: 2023-10-08 09:23+0000\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+0000\n"
"Last-Translator: Linus Hope, 2023\n"
"Language-Team: Italian (Italy) (https://app.transifex.com/alliance-auth/teams/107430/it_IT/)\n"
"MIME-Version: 1.0\n"
@ -30,7 +30,7 @@ msgstr "Google Analytics Universal"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Per completare questa azione è necessario un personaggio principale. "
@ -45,63 +45,68 @@ msgstr "Indirizzo di posta elettronica"
msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "Non ti è consentito aggiungere o rimuovere questi gruppi limitati:%s"
#: allianceauth/authentication/models.py:80
#: allianceauth/authentication/models.py:71
msgid "English"
msgstr "Inglese"
#: allianceauth/authentication/models.py:81
#: allianceauth/authentication/models.py:72
msgid "German"
msgstr "Tedesco"
#: allianceauth/authentication/models.py:82
#: allianceauth/authentication/models.py:73
msgid "Spanish"
msgstr "Spagnolo"
#: allianceauth/authentication/models.py:83
#: allianceauth/authentication/models.py:74
msgid "Chinese Simplified"
msgstr "Cinese semplificato"
#: allianceauth/authentication/models.py:84
#: allianceauth/authentication/models.py:75
msgid "Russian"
msgstr "Russo"
#: allianceauth/authentication/models.py:85
#: allianceauth/authentication/models.py:76
msgid "Korean"
msgstr "Coreano"
#: allianceauth/authentication/models.py:86
#: allianceauth/authentication/models.py:77
msgid "French"
msgstr "Francese"
#: allianceauth/authentication/models.py:87
#: allianceauth/authentication/models.py:78
msgid "Japanese"
msgstr "Giapponese"
#: allianceauth/authentication/models.py:88
#: allianceauth/authentication/models.py:79
msgid "Italian"
msgstr "Italiano"
#: allianceauth/authentication/models.py:91
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Lingua"
#: allianceauth/authentication/models.py:96
#: allianceauth/authentication/models.py:101
#: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode"
msgstr "Modalità scura"
#: allianceauth/authentication/models.py:110
#: allianceauth/authentication/models.py:115
#, python-format
msgid "State changed to: %s"
msgstr "Stato modificato a: %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Il tuo stato utente è ora: %(state)s"
#: 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 "Pannello di controllo"
@ -160,8 +165,50 @@ msgstr "Corporazione"
msgid "Alliance"
msgstr "Alleanza"
#: 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 "Azioni"
#: 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 "Personaggio"
#: 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 "Accedi"
@ -195,7 +242,7 @@ msgstr "Registrati"
msgid "Invalid or expired activation link."
msgstr "Il link di attivazione è invalido o scaduto."
#: 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 "
@ -204,32 +251,34 @@ msgstr ""
"Il seguente personaggio %(char)s non può essere reso principale: è già "
"utilizzato da un altro profilo."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Hai reso personaggio principale: %(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "%(name)s è stato aggiunto al tuo profilo."
#: 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 ""
"Impossibile aggiungere %(name)s al tuo profilo: quel personaggio è già "
"collegato ad un altro profilo."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Impossibile autenticarsi con il personaggio selezionato."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Il token di registrazione è scaduto."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
@ -237,13 +286,13 @@ msgstr ""
"Una e-mail di conferma è stata inviata. Per favore, utilizza il link per "
"confermare il tuo indirizzo di posta elettronica."
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Il tuo indirizzo di posta elettronica è stato confermato. Per favore accedi "
"per continuare."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "Al momento non è possibile registrare nuovi profili."
@ -286,19 +335,6 @@ msgstr "Non registrati"
msgid "Last update:"
msgstr "Ultimo aggiornamento:"
#: 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 "Personaggio"
#: allianceauth/corputils/templates/corputils/corpstats.html:75
#: allianceauth/corputils/templates/corputils/search.html:14
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:31
@ -638,19 +674,24 @@ msgstr ""
msgid "Group Management"
msgstr "Gestione gruppi"
#: allianceauth/groupmanagement/forms.py:15
#: allianceauth/groupmanagement/forms.py:18
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Utenti"
#: allianceauth/groupmanagement/forms.py:52
msgid "This name has been reserved and can not be used for groups."
msgstr "Questo nome è riservato e non può essere utilizzato per gruppi."
#: allianceauth/groupmanagement/forms.py:25
#: allianceauth/groupmanagement/forms.py:62
msgid "(auto)"
msgstr "(auto)"
#: allianceauth/groupmanagement/forms.py:34
#: allianceauth/groupmanagement/forms.py:71
msgid "There already exists a group with that name."
msgstr "Esiste già un gruppo con quel nome."
#: 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_* "
@ -661,13 +702,13 @@ msgstr ""
"Alleanze, etc. <br><b>Sovrascrive opzioni nascoste e aperte quando "
"seleazionato."
#: 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 ""
"Il gruppo è nascosto agli utenti ma questi vi possono aderire utilizzando il"
" link corretto. "
#: 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."
@ -676,7 +717,7 @@ msgstr ""
"di richiesta. <br>Se il gruppo non è aperto gli utenti necessiteranno di "
"approvazione manuale."
#: 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 "
@ -688,7 +729,7 @@ msgstr ""
"gruppo. <br>Auth non rimuoverà automaticamente gli utenti da questo gruppo "
"quando non sono più autenticati."
#: 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."
@ -696,7 +737,7 @@ msgstr ""
"Il gruppo è limitato. Ciò significa che l'aggiunta o la rimozione di utenti "
"per questo gruppo richiede un amministratore."
#: 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 "
@ -706,7 +747,7 @@ msgstr ""
"l'<code>autorizzazione auth.group_management </code> per consentire ad un "
"utente di gestire tutti i gruppi. <br> "
#: 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 "
@ -716,7 +757,7 @@ msgstr ""
" Usa l'<code>autorizzazione auth.group_management </code> per consentire ad "
"un utente di gestire tutti i gruppi. <br> "
#: 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>"
@ -724,42 +765,42 @@ msgstr ""
"Gli stati qui elencati avranno la possibilità di aderire a questo gruppo a "
"condizione che dispongano delle opportune autorizzazioni. <br> "
#: 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 ""
"Breve descrizione <i>(max. 512 caratteri)</i> del gruppo da mostrare agli "
"utenti."
#: allianceauth/groupmanagement/models.py:178
#: allianceauth/groupmanagement/models.py:177
msgid "Can request non-public groups"
msgstr "Può fare richiesta a gruppi non pubblici"
#: allianceauth/groupmanagement/models.py:209
#: allianceauth/groupmanagement/models.py:208
msgid "name"
msgstr "nome"
#: allianceauth/groupmanagement/models.py:212
#: allianceauth/groupmanagement/models.py:211
msgid "Name that can not be used for groups."
msgstr "Il nome scelto non può essere utilizzato per gruppi."
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "reason"
msgstr "ragione"
#: allianceauth/groupmanagement/models.py:215
#: allianceauth/groupmanagement/models.py:214
msgid "Reason why this name is reserved."
msgstr "Ragione per la quale questo nome è riservato."
#: allianceauth/groupmanagement/models.py:218
#: allianceauth/groupmanagement/models.py:217
msgid "created by"
msgstr "creato da"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "created at"
msgstr "creato il"
#: allianceauth/groupmanagement/models.py:223
#: allianceauth/groupmanagement/models.py:222
msgid "Date when this entry was created"
msgstr "Data in cui è stata creata questa voce."
@ -986,26 +1027,26 @@ msgstr "Richieste"
msgid "Group Membership"
msgstr "Membri del gruppo"
#: allianceauth/groupmanagement/views.py:163
#: allianceauth/groupmanagement/views.py:166
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "Rimosso il membro %(user)s dal gruppo %(group)s."
#: allianceauth/groupmanagement/views.py:165
#: allianceauth/groupmanagement/views.py:168
msgid "User does not exist in that group"
msgstr "Lutente non fa parte del gruppo selezionato"
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:171
msgid "Group does not exist"
msgstr "Il gruppo non esiste"
#: allianceauth/groupmanagement/views.py:195
#: allianceauth/groupmanagement/views.py:198
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "La domanda di %(mainchar)s per %(group)s è stata accettata."
#: 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 "
@ -1014,20 +1055,20 @@ msgstr ""
"Si è verificato unerrore durante lelaborazione della domanda di "
"%(mainchar)s per %(group)s."
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "La domanda di %(mainchar)s per %(group)s è stata rifiutata."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr ""
"La domanda di congedo da parte di %(mainchar)s per %(group)s è stata "
"accettata."
#: 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 "
@ -1036,45 +1077,45 @@ msgstr ""
"Si è verificato unerrore durante lelaborazione della domanda di congedo da"
" parte di %(mainchar)s per %(group)s."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr ""
"La domanda di congedo da parte di %(mainchar)s per %(group)s è stata "
"rifiutata."
#: 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 "Non puoi unirti a questo gruppo"
#: allianceauth/groupmanagement/views.py:341
#: allianceauth/groupmanagement/views.py:344
msgid "You are already a member of that group."
msgstr "Fai già parte del gruppo selezionato."
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:361
msgid "You already have a pending application for that group."
msgstr ""
"Hai già una candidatura in attesa di essere elaborata per questo gruppo"
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:370
#, python-format
msgid "Applied to group %(group)s."
msgstr "Hai fatto domanda per il gruppo %(group)s."
#: allianceauth/groupmanagement/views.py:377
#: allianceauth/groupmanagement/views.py:380
msgid "You cannot leave that group"
msgstr "Non puoi lasciare questo gruppo."
#: allianceauth/groupmanagement/views.py:381
#: allianceauth/groupmanagement/views.py:384
msgid "You are not a member of that group"
msgstr "Non sei un membro di questo gruppo."
#: allianceauth/groupmanagement/views.py:393
#: allianceauth/groupmanagement/views.py:396
msgid "You already have a pending leave request for that group."
msgstr "Hai già una richiesta di congedo in sospeso per quel gruppo."
#: allianceauth/groupmanagement/views.py:409
#: allianceauth/groupmanagement/views.py:412
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Hai fatto domanda di congedo per %(group)s."
@ -1136,16 +1177,6 @@ msgstr "Crea una domanda"
msgid "Username"
msgstr "Nome utente"
#: 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 "Azioni"
#: allianceauth/hrapplications/templates/hrapplications/management.html:38
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
#: allianceauth/hrapplications/templates/hrapplications/management.html:143
@ -1484,10 +1515,6 @@ msgstr "Modello"
msgid "Code Name"
msgstr "Nome del codice"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:35
msgid "Users"
msgstr "Utenti"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:41
msgid "States"
msgstr "Stati"
@ -2233,14 +2260,12 @@ 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 ""
"\n"
"%(queue_length)scompiti in coda"
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2255,11 +2280,11 @@ msgid "AA Support Discord"
msgstr "AA Discord di supporto"
#: 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 "Menu utente"
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:56
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:67
msgid "Logout"
msgstr "Disconnettersi"
@ -2315,22 +2340,30 @@ msgid "Objective"
msgstr "Obiettivo"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Giorni rimanenti"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Ore rimanenti"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Minuti rimanenti "
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Importante"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Limitato alla corporazione"

View File

@ -4,8 +4,8 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Foch Petain <brigadier.rockforward@gmail.com>, 2023
# kotaneko, 2023
# Foch Petain <brigadier.rockforward@gmail.com>, 2023
# Joel Falknau <ozirascal@gmail.com>, 2023
#
#, fuzzy
@ -13,8 +13,8 @@ msgid ""
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: 2023-10-08 09:23+0000\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+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"
@ -31,7 +31,7 @@ msgstr "Google ユニバーサル アナリティクス"
msgid "Google Analytics V4"
msgstr "Google アナリティクス 4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr "実行するためにはメインキャラクターの設定が必要です。設定してください。"
@ -44,63 +44,68 @@ msgstr "メールアドレス"
msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "これらの制限付きグループを追加または削除することはできません。%s"
#: 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
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "言語"
#: allianceauth/authentication/models.py:96
#: 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 "分類が%sに変更されました。"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "あなたの分類は%(state)sになりました。"
#: 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 "ダッシュボード"
@ -158,8 +163,50 @@ msgstr "Corp"
msgid "Alliance"
msgstr "Alliance"
#: 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 "ログイン"
@ -191,47 +238,49 @@ 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 "メインキャラクターを%(char)sへ変更できません。別のアカウントによって利用されています。"
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "メインキャラクターを%(char)sへ変更しました。"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "%(name)sをアカウントに追加しました。"
#: 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 "%(name)sをアカウントに追加することができません。すでに他のアカウントを持っています。"
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "選択されたキャラクターの認証が行えませんでした。"
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Registrationトークンが有効期限切れです。"
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "確認のメールを送信しました。メール内のリンクをご確認の上、メールアドレスの認証を完了させてください。"
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr "メールアドレスを確認しました。続行するにはログインしてください。"
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "新規アカウントの登録は、現時点ではできません。"
@ -274,19 +323,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
@ -616,19 +652,24 @@ msgstr "{character.character_name} のフリート参加を登録できません
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 "(auto)"
#: 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_* "
@ -637,18 +678,18 @@ msgstr ""
"内部グループです。ユーザーはこのグループを表示したり、参加したり、参加をリクエストしたりすることはできません。<br>Members、Corp_*、Alliance_*などのグループに使用します。選択すると、非表示"
" オプションと 開く <br> <b> オプションが上書きされます。</b> "
#: 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 ""
"グループはオープンで、ユーザーはリクエストに応じて自動的に追加されます。<br>グループがオープンでない場合、ユーザーのリクエストは手動で承認する必要があります。"
#: 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 "
@ -658,13 +699,13 @@ msgstr ""
"グループは一般公開されています。登録ユーザーなら誰でもこのグループに参加でき、このグループに設定されている他のオプションに基づいて表示されます。<br>認証されなくなっても、Auth"
" はユーザーをこのグループから自動的に削除しません。"
#: 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 "
@ -673,7 +714,7 @@ msgstr ""
"グループリーダーは、このグループのリクエストを処理できます。<code>auth.group_management </code> "
"権限を使用して、ユーザーがすべてのグループを管理できるようにします。<br> "
#: 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 "
@ -682,46 +723,46 @@ msgstr ""
"リーダーグループのメンバーは、このグループのリクエストを処理できます。<code>auth.group_management </code> "
"権限を使用して、ユーザーがすべてのグループを管理できるようにします。<br> "
#: 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 "ここに記載されているStatesは、適切な許可があれば、このグループに参加できます。<br> "
#: 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 "ユーザーに表示されるグループの簡単な説明 <i> (最大 512 文字) </i>。"
#: 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 "このエントリが作成された日付"
@ -948,86 +989,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 "%(user)sを%(group)sから削除する。"
#: 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 "%(mainchar)sからの%(group)sへの参加申請を承認しました。"
#: 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 "%(mainchar)sからの%(group)sへの参加申請を処理中にエラーが発生しました。"
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)sからの%(group)sへの参加申請は拒否されました。"
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)sからの%(group)sからの脱退申請は承認されました。"
#: 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 "%(mainchar)sからの%(group)sからの脱退申請を処理中にエラーが発生しました。"
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)sの%(group)sからの脱退申請は拒否されました。"
#: 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 "このGroupには入れません"
#: allianceauth/groupmanagement/views.py:341
#: allianceauth/groupmanagement/views.py:344
msgid "You are already a member of that group."
msgstr "すでにその Group に参加してます。"
#: 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 "%(group)sへの参加申請を送信しました。"
#: allianceauth/groupmanagement/views.py:377
#: allianceauth/groupmanagement/views.py:380
msgid "You cannot leave that group"
msgstr "この Group から脱退することはできません"
#: allianceauth/groupmanagement/views.py:381
#: allianceauth/groupmanagement/views.py:384
msgid "You are not a member of that group"
msgstr "あなたはその Group のメンバーではありません"
#: 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 "%(group)sからの脱退申請を送信しました。"
@ -1089,16 +1130,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
@ -1437,10 +1468,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 "States"
@ -2171,15 +2198,12 @@ 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 ""
"\n"
" %(queue_length)sキューに入っているタスク\n"
" "
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2194,11 +2218,11 @@ msgid "AA Support Discord"
msgstr "AA サポートディスコード"
#: 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 "ログアウト"
@ -2254,22 +2278,30 @@ msgid "Objective"
msgstr "目標"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "残り日数"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "残り時間"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "残り分数"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "重要"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "コーポレーション制限付き"

View File

@ -4,22 +4,22 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# 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
# None None <khd1226543@gmail.com>, 2023
# ThatRagingKid, 2023
# Lahty <js03js70@gmail.com>, 2023
# jackfrost, 2023
# Olgeda Choi <undead.choi@gmail.com>, 2023
# Seowon Jung <seowon@hawaii.edu>, 2023
# Alpha, 2023
#
#, fuzzy
msgid ""
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: 2023-10-08 09:23+0000\n"
"Last-Translator: jackfrost, 2023\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+0000\n"
"Last-Translator: Alpha, 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"
@ -35,7 +35,7 @@ msgstr "Google 애널리틱스 유니버설"
msgid "Google Analytics V4"
msgstr "Google 애널리틱스 V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr "해당 기능을 수행하려면 주 캐릭터가 요구됩니다. 아래에서 하나를 추가하시오."
@ -48,63 +48,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 "상태가 %s로 변경됐습니다."
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "사용자의 상태는 %(state)s입니다."
#: 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 "대시보드"
@ -163,8 +168,50 @@ 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 "로그인"
@ -196,47 +243,49 @@ 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 "%(char)s를 주 캐릭터로 변경할 수 없음: 다른 계정이 해당 캐릭터를 소유하고 있습니다."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "주 캐릭터가 %(char)s로 변경됨"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "계정에 %(name)s를 추가했습니다."
#: 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 "계정에 %(name)s를 추가하지 못했습니다. 이미 다른 계정에 추가되었습니다."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "선택한 캐릭터로 인증할 수 없습니다."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "가입 토큰이 만료되었습니다."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "확인 메일 전송됨. 다음 링크를 눌러 이메일 주소를 확인하세요."
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr "이메일 주소가 확인되었습니다. 로그인 해주세요."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "현재 새로운 계정 등록은 받지않습니다."
@ -279,19 +328,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
@ -621,19 +657,24 @@ 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_* "
@ -642,11 +683,11 @@ msgstr ""
"시스템 그룹, 유저들은 이 그룹을 보거나, 참여하거나, 지원할 수 없습니다. <br>멤버, 코퍼레이션_*, 얼라이언스_* 등에 "
"사용됨.<br><b>선택된 경우 비공개와 공개 옵션을 무시함.</b>"
#: 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."
@ -654,7 +695,7 @@ msgstr ""
"그룹은 공개되어 있으며 요청 시 유저는 자동적으로 추가됩니다.<br>그룹이 공개되어 있지 않은 경우, 유저는 직접 요청을 승인받아야 "
"합니다."
#: 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 "
@ -664,13 +705,13 @@ msgstr ""
"공개 그룹입니다. 등록된 모든 유저는 이 그룹에 참여할 수 있으며, 이 그룹의 설정에 따라 공개 여부가 달라집니다.<br>유저가 더 "
"이상 인증을 하지 않을 때, Auth는 이 그룹에서 유저를 자동 추방하지 않습니다."
#: 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 "
@ -679,7 +720,7 @@ msgstr ""
"그룹 리더는 이 그룹의 요청을 처리할 수 있습니다. <code>auth.group_management</code> 권한을 사용하여 "
"사용자가 모든 그룹을 관리할 수 있도록 합니다.<br>"
#: 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 "
@ -688,46 +729,46 @@ msgstr ""
"리더 그룹의 구성원은 이 그룹에 대한 요청을 처리할 수 있습니다. <code>1auth.group_management1</code> "
"권한을 사용하여 사용자가 모든 그룹을 관리할 수 있도록 합니다.<br>"
#: 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 "사용자에게 나타나는 그룹에 대한 간단한 설명 <i>(최대 512자)</i> 입니다."
#: 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 "이 항목이 생성된 날짜"
@ -954,86 +995,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 "유저 %(user)s이(가) %(group)s에서 제거됨."
#: 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 "%(mainchar)s의 %(group)s 그룹 신청 수락"
#: 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 "%(mainchar)s의 %(group)s 그룹 신청을 처리하는 중 알 수 없는 에러 발생"
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s의 %(group)s 그룹 신청 거절"
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s의 %(group)s 그룹 탈퇴 수락"
#: 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 "%(mainchar)s의 %(group)s 그룹 탈퇴를 처리하는 중 알 수 없는 에러 발생"
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s의 %(group)s 그룹 탈퇴 거절"
#: 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 "%(group)s그룹에 지원하였음."
#: 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 "%(group)s그룹의 탈퇴가 신청됨."
@ -1095,16 +1136,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
@ -1443,10 +1474,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 "상태"
@ -2170,11 +2197,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
@ -2190,11 +2217,11 @@ msgid "AA Support Discord"
msgstr "AA Support Discord"
#: 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 "로그아웃"
@ -2250,22 +2277,30 @@ msgid "Objective"
msgstr "목표 대상"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "남은 일수"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "남은 시간"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "남은 분"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "중요"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "코퍼레이션 제한"

View File

@ -4,8 +4,8 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Yuriy K <thedjcooltv@gmail.com>, 2023
# Андрей Зубков <and.vareba81@gmail.com>, 2023
# Yuriy K <thedjcooltv@gmail.com>, 2023
# Alexander Gess <de.alex.gess@gmail.com>, 2023
# Filipp Chertiev <f@fzfx.ru>, 2023
#
@ -14,8 +14,8 @@ msgid ""
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: 2023-10-08 09:23+0000\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+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"
@ -32,7 +32,7 @@ msgstr "Google Analytics Universal"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Для продолжения следует указать основного персонажа. Выберите его ниже."
@ -46,63 +46,68 @@ msgstr "Email"
msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "Вам не разрешено добавлять или удалять эти ограниченные группы: %s"
#: 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
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Язык"
#: allianceauth/authentication/models.py:96
#: 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 "Статус изменен: %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Статус пилота: %(state)s"
#: 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 "Панель показателей"
@ -161,8 +166,50 @@ 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 "Вход"
@ -195,7 +242,7 @@ 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 "
@ -203,40 +250,42 @@ msgid ""
msgstr ""
"Нельзя сменить основного персонажа на %(char)s: похоже, что Владелец не Вы. "
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Основной персонаж заменен на %(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "Добавлен %(name)s на Ваш аккаунт."
#: 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 "Персонаж %(name)s уже добавлен."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Невозможно авторизировать этого персонажа. "
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Регистрационный токен просрочен."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "Отправить подтверждающее письмо. Пожалуйста, подтвердите почту. "
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr "Подтвердите Ваш email адрес. Зайти для подтверждения. "
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "Регистрация новых аккаунтов в настоящее время невозможна."
@ -279,19 +328,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
@ -629,20 +665,25 @@ 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_* "
@ -653,13 +694,13 @@ msgstr ""
"Members, Corp_*, Alliance_* и т. п.<br><b>Будучи выбранной, отменяет "
"настройки \"Скрытая\" и \"Открытая\".</b>"
#: 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."
@ -668,7 +709,7 @@ msgstr ""
"при отправке запроса.<br>Если группа не является открытой, запросы от "
"пользователей будут требовать ручного подтверждения."
#: 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 "
@ -680,7 +721,7 @@ msgstr ""
"настройках данной группы.<br>Auth не будет удалять пользователей из этой "
"группы автоматически при окончании срока их аутентификации."
#: 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."
@ -688,7 +729,7 @@ msgstr ""
"Группа является ограниченной. Это значит что добавление пользователей в эту "
"группу или удаление из неё требует прав superuser admin."
#: 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 "
@ -698,7 +739,7 @@ msgstr ""
"Используйте разрешение <code>auth.group_management</code>, чтобы позволить "
"пользователю управлять всеми группами.<br>"
#: 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 "
@ -708,7 +749,7 @@ msgstr ""
"Используйте разрешение <code>auth.group_management</code>, чтобы позволить "
"пользователю управлять всеми группами.<br>"
#: 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>"
@ -716,42 +757,42 @@ msgstr ""
"Статусы, перечисленные здесь, смогут присоединиться к группе, если у них "
"есть соответствующие разрешения.<br>"
#: 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 ""
"Краткое описание <i>(макс. 512 символов)</i> группы, отображаемое "
"пользователям."
#: 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 "Дата, когда данное содержимое было создано"
@ -978,26 +1019,26 @@ 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 "Пользователь %(user)s исключен из %(group)s."
#: 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 "Запрос от %(mainchar)sв %(group)s принят."
#: 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 "
@ -1006,18 +1047,18 @@ msgstr ""
"Персонаж %(mainchar)s не может быть добавлен %(group)s, из-за непредвиденной"
" ошибки. "
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s исключен из %(group)s."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Утвержден выход %(mainchar)s из %(group)s. "
#: 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 "
@ -1026,42 +1067,42 @@ msgstr ""
"Возникла ошибка во время обработки %(mainchar)s на выход из группы "
"%(group)s. Повторите позже."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Прошение об исключении %(mainchar)s из %(group)s отклонено. "
#: 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 "Вступить в группу %(group)s."
#: 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 "Запрос на выход из группы %(group)s."
@ -1123,16 +1164,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
@ -1471,10 +1502,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 "Статусы"
@ -2216,14 +2243,12 @@ msgstr ""
" Статус %(total)s обработанных задач • последние %(latest)s"
#: 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 ""
"\n"
" %(queue_length)s запланированных задач"
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2238,11 +2263,11 @@ msgid "AA Support Discord"
msgstr "Discord поддержки AA"
#: 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 "Выход"
@ -2298,22 +2323,30 @@ msgid "Objective"
msgstr "Задача"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Дней осталось"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Часов осталось"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Минут осталось"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Важно"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Корпорация зарегистрированна"

View File

@ -12,8 +12,8 @@ msgid ""
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: 2023-10-08 09:23+0000\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+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"
@ -30,7 +30,7 @@ msgstr "Універсальна Google Аналітика"
msgid "Google Analytics V4"
msgstr "Google Analytics V4"
#: allianceauth/authentication/decorators.py:37
#: allianceauth/authentication/decorators.py:49
msgid "A main character is required to perform that action. Add one below."
msgstr ""
"Для виконання цієї дії потрібен основний персонаж. Додайте його нижче."
@ -44,63 +44,68 @@ msgstr "Електронна пошта"
msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "Вам заборонено додавати або видаляти ці обмежені групи: %s"
#: 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
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "Мова"
#: allianceauth/authentication/models.py:96
#: 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 "Стан змінено на: %s"
#: allianceauth/authentication/models.py:111
#: allianceauth/authentication/models.py:116
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Стан вашого користувача зараз: %(state)s"
#: 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 "Панель приладів"
@ -158,8 +163,50 @@ 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 "Увійти"
@ -193,7 +240,7 @@ 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 "
@ -202,32 +249,34 @@ msgstr ""
"Неможливо змінити основного персонажа на %(char)s: персонаж належить іншому "
"акаунту."
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "Основний персонаж змінено на %(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "Додано %(name)s до вашого облікового запису."
#: 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 ""
"Не вдалося додати %(name)s до вашого облікового запису: у них вже є "
"обліковий запис."
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "Не вдалося автентифікуватися як обраний персонаж."
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "Токен реєстрації застарів."
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
@ -235,13 +284,13 @@ msgstr ""
"Відправлено лист з підтвердженням. Будь ласка, перейдіть за посиланням, щоб "
"підтвердити свою адресу електронної пошти."
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr ""
"Підтверджено вашу адресу електронної пошти. Будь ласка, увійдіть, щоб "
"продовжити."
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr "Реєстрація нових облікових записів наразі не дозволена."
@ -284,19 +333,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
@ -634,19 +670,24 @@ 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_* "
@ -657,12 +698,12 @@ msgstr ""
"\"Members, Corp_, Alliance_ і т.д.<br><b>Перевизначає параметри Hidden і \"\n"
"\"Open при виборі.</b>\""
#: 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."
@ -671,7 +712,7 @@ msgstr ""
"запитом.<br>Якщо група закрита, користувачі повинні отримати ручне "
"підтвердження запиту."
#: 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 "
@ -683,7 +724,7 @@ msgstr ""
"групи.<br>Авторизація не буде автоматично видаляти користувачів з цієї "
"групи, коли вони більше не автентифіковані."
#: 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."
@ -691,7 +732,7 @@ 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 "
@ -701,7 +742,7 @@ msgstr ""
"<code>auth.group_management</code>, щоб дозволити користувачеві керувати "
"всіма групами.<br>"
#: 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 "
@ -711,7 +752,7 @@ msgstr ""
" дозвіл <code>auth.group_management</code>, щоб дозволити користувачеві "
"керувати всіма групами.<br>"
#: 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>"
@ -719,42 +760,42 @@ msgstr ""
"Штати, перераховані тут, матимуть змогу приєднатися до цієї групи, якщо вони"
" мають відповідні дозволи.<br>"
#: 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 ""
"Короткий опис <i>(максимум 512 символів)</i> групи, що відображається "
"користувачам."
#: 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 "Дата створення цього запису"
@ -981,26 +1022,26 @@ 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 "Користувач %(user)s вилучений з групи %(group)s."
#: 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 "Заявка %(mainchar)s на вступ до %(group)s прийнята."
#: 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 "
@ -1009,18 +1050,18 @@ msgstr ""
"Під час обробки заявки %(mainchar)s на вступ до %(group)s виникла помилка, "
"яку не можна обробити."
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Заявка %(mainchar)s на вступ до %(group)s відхилена."
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Заявка %(mainchar)s на вихід з %(group)s прийнята."
#: 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 "
@ -1029,42 +1070,42 @@ msgstr ""
"Під час обробки заявки %(mainchar)s на вихід з %(group)s виникла помилка, "
"яку не можна обробити."
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Заявка %(mainchar)s на вихід з %(group)s відхилена."
#: 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 "Подано заявку на групу %(group)s."
#: 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 "Подано заявку на вихід з групи %(group)s."
@ -1126,16 +1167,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
@ -1474,10 +1505,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 "Стани"
@ -2220,14 +2247,12 @@ msgstr ""
"Статус %(total)s виконаних завдань • останній %(latest)s"
#: 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 ""
"\n"
"%(queue_length)s завдань в черзі"
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin"
@ -2242,11 +2267,11 @@ msgid "AA Support Discord"
msgstr "Підтримка AA у Discord"
#: 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 "Вихід"
@ -2302,22 +2327,30 @@ msgid "Objective"
msgstr "Мета"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "Залишилося днів"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "Залишилося годин"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "Залишилося хвилин"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "Важливо"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "Обмежено для корпорації"

View File

@ -4,19 +4,19 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Shen Yang, 2023
# Jesse . <sgeine@hotmail.com>, 2023
# Aaron BuBu <351793078@qq.com>, 2023
# Joel Falknau <ozirascal@gmail.com>, 2023
# Shen Yang, 2023
#
#, fuzzy
msgid ""
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: 2023-10-08 09:23+0000\n"
"Last-Translator: Shen Yang, 2023\n"
"POT-Creation-Date: 2024-02-17 18:50+1000\n"
"PO-Revision-Date: 2023-11-08 13:50+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 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"
@ -32,7 +32,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 "只有主要角色才能执行这个操作。在下面添加一个"
@ -45,63 +45,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
#: allianceauth/authentication/models.py:80
msgid "Ukrainian"
msgstr ""
#: allianceauth/authentication/models.py:96
msgid "Language"
msgstr "语言"
#: allianceauth/authentication/models.py:96
#: 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 "账户总览"
@ -157,8 +162,50 @@ 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 "登录"
@ -190,47 +237,49 @@ 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 "不能修改主角色为%(char)s这个角色被另一个账户所拥有"
#: allianceauth/authentication/views.py:83
#: allianceauth/authentication/views.py:124
#, python-format
msgid "Changed main character to %(char)s"
msgstr "修改主要角色为%(char)s"
#: allianceauth/authentication/views.py:92
#: allianceauth/authentication/views.py:133
#, python-format
msgid "Added %(name)s to your account."
msgstr "添加%(name)s到您的账户"
#: 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 "添加%(name)s到您的账户失败他们已经在一个账户中了"
#: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character."
msgstr "无法作为选定的角色进行身份验证"
#: allianceauth/authentication/views.py:178
msgid ""
"Unable to authenticate as the selected character. Please log in with the "
"main character associated with this account."
msgstr ""
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:244
msgid "Registration token has expired."
msgstr "注册令牌过期。"
#: allianceauth/authentication/views.py:252
#: allianceauth/authentication/views.py:302
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "已经发送了确认邮件。请按照链接确定您的电邮地址"
#: allianceauth/authentication/views.py:257
#: allianceauth/authentication/views.py:307
msgid "Confirmed your email address. Please login to continue."
msgstr "已确认您的电邮地址。请登录以继续"
#: allianceauth/authentication/views.py:262
#: allianceauth/authentication/views.py:312
msgid "Registration of new accounts is not allowed at this time."
msgstr ""
@ -273,19 +322,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
@ -615,36 +651,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 "
@ -652,66 +693,66 @@ 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 ""
@ -938,86 +979,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 "已将用户%(user)s从用户组%(group)s中移除"
#: 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 "已接受用户%(mainchar)s加入%(group)s的申请"
#: 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 "在处理用户%(mainchar)s加入%(group)s的申请的过程中出现了一个搞不定的错误"
#: allianceauth/groupmanagement/views.py:226
#: allianceauth/groupmanagement/views.py:229
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已拒绝"
#: allianceauth/groupmanagement/views.py:261
#: allianceauth/groupmanagement/views.py:264
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已通过"
#: 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 "在处理%(mainchar)s离开%(group)s的程序时发生了未知的错误"
#: allianceauth/groupmanagement/views.py:292
#: allianceauth/groupmanagement/views.py:295
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s离开%(group)s的请求已被拒绝"
#: 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 "修改已经应用到%(group)s啦"
#: 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 "已经离开群组%(group)s"
@ -1079,16 +1120,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
@ -1427,10 +1458,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 "声望"
@ -2153,11 +2180,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
@ -2173,11 +2200,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 "登出"
@ -2233,22 +2260,30 @@ msgid "Objective"
msgstr "声望"
#: allianceauth/timerboard/form.py:64
msgid "Absolute Timer"
msgstr ""
#: allianceauth/timerboard/form.py:65
msgid "Date and Time"
msgstr ""
#: allianceauth/timerboard/form.py:66
msgid "Days Remaining"
msgstr "剩余天数"
#: allianceauth/timerboard/form.py:65
#: allianceauth/timerboard/form.py:67
msgid "Hours Remaining"
msgstr "剩余小时数"
#: allianceauth/timerboard/form.py:67
#: allianceauth/timerboard/form.py:69
msgid "Minutes Remaining"
msgstr "剩余分钟"
#: allianceauth/timerboard/form.py:69
#: allianceauth/timerboard/form.py:71
msgid "Important"
msgstr "重要信息"
#: allianceauth/timerboard/form.py:70
#: allianceauth/timerboard/form.py:72
msgid "Corp-Restricted"
msgstr "受限制的公司"

View File

@ -1,9 +1,111 @@
"""Admin site for menu app."""
from typing import Optional
from django.contrib import admin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_noop as _
from . import models
from .constants import MenuItemType
from .core.smart_sync import sync_menu
from .filters import MenuItemTypeListFilter
from .forms import (
AppMenuItemAdminForm,
FolderMenuItemAdminForm,
LinkMenuItemAdminForm,
)
from .models import MenuItem
@admin.register(models.MenuItem)
@admin.register(MenuItem)
class MenuItemAdmin(admin.ModelAdmin):
list_display = ['text', 'hide', 'parent', 'url', 'icon_classes', 'rank']
ordering = ('rank',)
list_display = (
"_text",
"parent",
"order",
"_user_defined",
"_visible",
"_children",
)
list_filter = [
MenuItemTypeListFilter,
"is_hidden",
("parent", admin.RelatedOnlyFieldListFilter),
]
ordering = ["parent", "order", "text"]
def get_form(self, request: HttpRequest, obj: Optional[MenuItem] = None, **kwargs):
kwargs["form"] = self._choose_form(request, obj)
return super().get_form(request, obj, **kwargs)
@classmethod
def _choose_form(cls, request: HttpRequest, obj: Optional[MenuItem]):
"""Return the form for the current menu item type."""
if obj: # change
if obj.hook_hash:
return AppMenuItemAdminForm
if obj.is_folder:
return FolderMenuItemAdminForm
return LinkMenuItemAdminForm
# add
if cls._type_from_request(request) is MenuItemType.FOLDER:
return FolderMenuItemAdminForm
return LinkMenuItemAdminForm
def add_view(self, request, form_url="", extra_context=None) -> HttpResponse:
context = extra_context or {}
item_type = self._type_from_request(request, default=MenuItemType.LINK)
context["title"] = _("Add %s menu item") % item_type.label
return super().add_view(request, form_url, context)
def change_view(
self, request, object_id, form_url="", extra_context=None
) -> HttpResponse:
extra_context = extra_context or {}
obj = get_object_or_404(MenuItem, id=object_id)
extra_context["title"] = _("Change %s menu item") % obj.item_type.label
return super().change_view(request, object_id, form_url, extra_context)
def changelist_view(self, request: HttpRequest, extra_context=None):
# needed to ensure items are updated after an app change
# and when the admin page is opened directly
sync_menu()
extra_context = extra_context or {}
extra_context["folder_type"] = MenuItemType.FOLDER.value
return super().changelist_view(request, extra_context)
@admin.display(description=_("children"))
def _children(self, obj: MenuItem):
if not obj.is_folder:
return []
names = [obj.text for obj in obj.children.order_by("order", "text")]
return names if names else "?"
@admin.display(description=_("text"), ordering="text")
def _text(self, obj: MenuItem) -> str:
if obj.is_folder:
return f"[{obj.text}]"
return obj.text
@admin.display(description=_("user defined"), boolean=True)
def _user_defined(self, obj: MenuItem) -> bool:
return obj.is_user_defined
@admin.display(description=_("visible"), ordering="is_hidden", boolean=True)
def _visible(self, obj: MenuItem) -> bool:
return not bool(obj.is_hidden)
@staticmethod
def _type_from_request(
request: HttpRequest, default=None
) -> Optional[MenuItemType]:
try:
return MenuItemType(request.GET.get("type"))
except ValueError:
return default

View File

@ -1,19 +1,19 @@
import logging
from django.apps import AppConfig
from django.db.utils import ProgrammingError, OperationalError
logger = logging.getLogger(__name__)
# TODO discuss permissions for user defined links
# TODO define aa way for hooks to predefine a "parent" to create a sub menu from modules
# TODO Add user documentation
class MenuConfig(AppConfig):
name = "allianceauth.menu"
label = "menu"
def ready(self):
try:
from allianceauth.menu.providers import menu_provider
menu_provider.clear_synced_flag()
except (ProgrammingError, OperationalError):
logger.warning("Migrations not completed for MenuItems")
from allianceauth.menu.core import smart_sync
smart_sync.reset_menu_items_sync()

View File

@ -0,0 +1,18 @@
"""Global constants for the menu app."""
from django.db import models
from django.utils.translation import gettext_lazy as _
DEFAULT_FOLDER_ICON_CLASSES = "fa-solid fa-folder" # TODO: Make this a setting?
"""Default icon class for folders."""
DEFAULT_MENU_ITEM_ORDER = 9999
"""Default order for any menu item."""
class MenuItemType(models.TextChoices):
"""The type of a menu item."""
APP = "app", _("app")
FOLDER = "folder", _("folder")
LINK = "link", _("link")

View File

@ -0,0 +1,48 @@
"""Logic for handling MenuItemHook objects."""
import hashlib
from typing import List, NamedTuple, Optional
from allianceauth.menu.hooks import MenuItemHook
class MenuItemHookCustom(MenuItemHook):
"""A user defined menu item that can be rendered with the standard template."""
def __init__(
self,
text: str,
classes: str,
url_name: str,
order: Optional[int] = None,
navactive: Optional[List[str]] = None,
):
super().__init__(text, classes, url_name, order, navactive)
self.url = ""
self.is_folder = None
self.html_id = ""
self.children = []
class MenuItemHookParams(NamedTuple):
"""Immutable container for params about a menu item hook."""
text: str
order: int
hash: str
def generate_hash(obj: MenuItemHook) -> str:
"""Return the hash for a menu item hook."""
my_class = obj.__class__
name = f"{my_class.__module__}.{my_class.__name__}"
hash_value = hashlib.sha256(name.encode("utf-8")).hexdigest()
return hash_value
def gather_params(obj: MenuItemHook) -> MenuItemHookParams:
"""Return params from a menu item hook."""
text = getattr(obj, "text", obj.__class__.__name__)
order = getattr(obj, "order", None)
hash = generate_hash(obj)
return MenuItemHookParams(text=text, hash=hash, order=order)

View File

@ -0,0 +1,30 @@
"""Provide capability to sync menu items when needed only."""
from django.core.cache import cache
_MENU_SYNC_CACHE_KEY = "ALLIANCEAUTH-MENU-SYNCED"
def sync_menu() -> None:
"""Sync menu items if needed only."""
from allianceauth.menu.models import MenuItem
is_sync_needed = not _is_menu_synced() or not MenuItem.objects.exists()
# need to also check for existence of MenuItems in database
# to ensure the menu is synced during tests
if is_sync_needed:
MenuItem.objects.sync_all()
_record_menu_was_synced()
def _is_menu_synced() -> bool:
return cache.get(_MENU_SYNC_CACHE_KEY, False)
def _record_menu_was_synced() -> None:
cache.set(_MENU_SYNC_CACHE_KEY, True, timeout=None) # no timeout
def reset_menu_items_sync() -> None:
"""Ensure menu items are synced, e.g. after a Django restart."""
cache.delete(_MENU_SYNC_CACHE_KEY)

View File

@ -0,0 +1,24 @@
"""Filters for the menu app."""
from django.contrib import admin
from django.utils.translation import gettext_noop as _
from allianceauth.menu.constants import MenuItemType
class MenuItemTypeListFilter(admin.SimpleListFilter):
"""Allow filtering admin changelist by menu item type."""
title = _("type")
parameter_name = "type"
def lookups(self, request, model_admin):
return [(obj.value, obj.label.title()) for obj in MenuItemType]
def queryset(self, request, queryset):
if value := self.value():
return queryset.annotate_item_type_2().filter(
item_type_2=MenuItemType(value).value
)
return None

View File

@ -0,0 +1,49 @@
from django import forms
from .constants import DEFAULT_FOLDER_ICON_CLASSES
from .models import MenuItem
class FolderMenuItemAdminForm(forms.ModelForm):
"""A form for changing folder items."""
class Meta:
model = MenuItem
fields = ["text", "classes", "order", "is_hidden"]
def clean(self):
data = super().clean()
if not data["classes"]:
data["classes"] = DEFAULT_FOLDER_ICON_CLASSES
return data
class _BasedMenuItemAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["parent"].queryset = MenuItem.objects.filter_folders().order_by(
"text"
)
self.fields["parent"].required = False
self.fields["parent"].widget = forms.Select(
choices=self.fields["parent"].widget.choices
) # disable modify buttons
class AppMenuItemAdminForm(_BasedMenuItemAdminForm):
"""A form for changing app items."""
class Meta:
model = MenuItem
fields = ["order", "parent", "is_hidden"]
class LinkMenuItemAdminForm(_BasedMenuItemAdminForm):
"""A form for changing link items."""
class Meta:
model = MenuItem
fields = ["text", "url", "classes", "order", "parent", "is_hidden"]
widgets = {
"url": forms.TextInput(attrs={"size": "100"}),
}

View File

@ -1,42 +1,58 @@
from django.template.loader import render_to_string
"""Menu item hooks."""
from typing import List, Optional
from django.template.loader import render_to_string
from allianceauth.menu.constants import DEFAULT_MENU_ITEM_ORDER
class MenuItemHook:
"""
Auth Hook for generating Side Menu Items
"""
def __init__(self, text: str, classes: str, url_name: str, order: Optional[int] = None, navactive: List = []):
"""
:param text: The text shown as menu item, e.g. usually the name of the app.
:type text: str
:param classes: The classes that should be applied to the menu item icon
:type classes: List[str]
:param url_name: The name of the Django URL to use
:type url_name: str
:param order: An integer which specifies the order of the menu item, lowest to highest. Community apps are free to use any order above `1000`. Numbers below are served for Auth.
:type order: Optional[int], optional
:param navactive: A list of views or namespaces the link should be highlighted on. See [django-navhelper](https://github.com/geelweb/django-navhelper#navactive) for usage. Defaults to the supplied `url_name`.
:type navactive: List, optional
"""
"""Auth Hook for generating side menu items.
Args:
- text: The text shown as menu item, e.g. usually the name of the app.
- classes: The classes that should be applied to the menu item icon
- url_name: The name of the Django URL to use
- order: An integer which specifies the order of the menu item,
lowest to highest. Community apps are free to use any order above `1000`.
Numbers below are served for Auth.
- A list of views or namespaces the link should be highlighted on.
See 3rd party package django-navhelper for usage.
Defaults to the supplied `url_name`.
Optional:
- count is an integer shown next to the menu item as badge when is is not `None`.
Apps need to set the count in their child class, e.g. in `render()` method
"""
def __init__(
self,
text: str,
classes: str,
url_name: str,
order: Optional[int] = None,
navactive: Optional[List[str]] = None,
):
self.text = text
self.classes = classes
self.url_name = url_name
self.template = 'public/menuitem.html'
self.order = order if order is not None else 9999
# count is an integer shown next to the menu item as badge when count != None
# apps need to set the count in their child class, e.g. in render() method
self.template = "public/menuitem.html"
self.order = order if order is not None else DEFAULT_MENU_ITEM_ORDER
self.count = None
navactive = navactive or []
navactive.append(url_name)
self.navactive = navactive
def render(self, request):
return render_to_string(self.template,
{'item': self},
request=request)
def __str__(self) -> str:
return self.text
def __repr__(self) -> str:
return f'{self.__class__.__name__}(text="{self.text}")'
def render(self, request) -> str:
"""Render this menu item and return resulting HTML."""
return render_to_string(self.template, {"item": self}, request=request)

View File

@ -0,0 +1,67 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from django.db import models
from django.db.models import Case, Q, Value, When
from allianceauth.hooks import get_hooks
from .constants import MenuItemType
from .core.menu_item_hooks import MenuItemHookParams, gather_params
if TYPE_CHECKING:
from .models import MenuItem
logger = logging.getLogger(__name__)
class MenuItemQuerySet(models.QuerySet):
def filter_folders(self):
"""Add filter to include folders only."""
return self.filter(hook_hash__isnull=True, url="")
def annotate_item_type_2(self):
"""Add calculated field with item type."""
return self.annotate(
item_type_2=Case(
When(~Q(hook_hash__isnull=True), then=Value(MenuItemType.APP.value)),
When(url="", then=Value(MenuItemType.FOLDER.value)),
default=Value(MenuItemType.LINK.value),
)
)
class MenuItemManagerBase(models.Manager):
def sync_all(self):
"""Sync all menu items from hooks."""
hook_params = self._gather_menu_item_hook_params()
self._delete_obsolete_app_items(hook_params)
self._update_or_create_app_items(hook_params)
def _gather_menu_item_hook_params(self) -> list[MenuItemHookParams]:
params = [gather_params(hook()) for hook in get_hooks("menu_item_hook")]
return params
def _delete_obsolete_app_items(self, params: list[MenuItemHookParams]):
hashes = [obj.hash for obj in params]
self.exclude(hook_hash__isnull=True).exclude(hook_hash__in=hashes).delete()
def _update_or_create_app_items(self, params: list[MenuItemHookParams]):
for param in params:
try:
obj: MenuItem = self.get(hook_hash=param.hash)
except self.model.DoesNotExist:
self.create(hook_hash=param.hash, order=param.order, text=param.text)
else:
# if it exists update the text only
if obj.text != param.text:
obj.text = param.text
obj.save()
logger.debug("Updated menu items from %d menu item hooks", len(params))
MenuItemManager = MenuItemManagerBase.from_queryset(MenuItemQuerySet)

View File

@ -1,15 +0,0 @@
from django.utils.deprecation import MiddlewareMixin
import logging
from allianceauth.menu.providers import menu_provider
logger = logging.getLogger(__name__)
class MenuSyncMiddleware(MiddlewareMixin):
def __call__(self, request):
"""Alliance Auth Menu Sync Middleware"""
menu_provider.check_and_sync_menu()
return super().__call__(request)

View File

@ -1,4 +1,4 @@
# Generated by Django 4.0.2 on 2022-08-28 14:00
# Generated by Django 4.2.9 on 2024-02-15 00:01
from django.db import migrations, models
import django.db.models.deletion
@ -8,21 +8,88 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='MenuItem',
name="MenuItem",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hook_function', models.CharField(max_length=500)),
('icon_classes', models.CharField(max_length=150)),
('text', models.CharField(max_length=150)),
('url', models.CharField(blank=True, default=None, max_length=2048, null=True)),
('rank', models.IntegerField(default=1000)),
('hide', models.BooleanField(default=False)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='menu.menuitem')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"text",
models.CharField(
db_index=True,
help_text="Text to show on menu",
max_length=150,
verbose_name="text",
),
),
(
"order",
models.IntegerField(
db_index=True,
default=9999,
help_text="Order of the menu. Lowest First",
verbose_name="order",
),
),
(
"is_hidden",
models.BooleanField(
default=False,
help_text="Hide this menu item.If this item is a folder all items under it will be hidden too",
verbose_name="is hidden",
),
),
(
"hook_hash",
models.CharField(
default=None,
editable=False,
max_length=64,
null=True,
unique=True,
),
),
(
"classes",
models.CharField(
blank=True,
default="",
help_text="Font Awesome classes to show as icon on menu, e.g. <code>fa-solid fa-house</code>",
max_length=150,
verbose_name="icon classes",
),
),
(
"url",
models.TextField(
default="",
help_text="External URL this menu items will link to",
verbose_name="url",
),
),
(
"parent",
models.ForeignKey(
blank=True,
default=None,
help_text="Folder this item is in (optional)",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="children",
to="menu.menuitem",
verbose_name="folder",
),
),
],
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 4.0.2 on 2022-08-28 14:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('menu', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='menuitem',
name='hook_function',
field=models.CharField(blank=True, default=None, max_length=500, null=True),
),
migrations.AlterField(
model_name='menuitem',
name='icon_classes',
field=models.CharField(blank=True, default=None, max_length=150, null=True),
),
migrations.AlterField(
model_name='menuitem',
name='text',
field=models.CharField(blank=True, default=None, max_length=150, null=True),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 4.0.8 on 2023-02-05 07:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('menu', '0002_alter_menuitem_hook_function_and_more'),
]
operations = [
migrations.AddIndex(
model_name='menuitem',
index=models.Index(fields=['rank'], name='menu_menuit_rank_e880ab_idx'),
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 4.0.10 on 2023-07-16 11:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('menu', '0003_menuitem_menu_menuit_rank_e880ab_idx'),
]
operations = [
migrations.AlterField(
model_name='menuitem',
name='hide',
field=models.BooleanField(default=False, help_text='Hide this menu item. If this item is a header all items under it will be hidden too.'),
),
migrations.AlterField(
model_name='menuitem',
name='icon_classes',
field=models.CharField(blank=True, default=None, help_text='Font Awesome classes to show as icon on menu', max_length=150, null=True),
),
migrations.AlterField(
model_name='menuitem',
name='parent',
field=models.ForeignKey(blank=True, help_text='Parent Header. (Optional)', null=True, on_delete=django.db.models.deletion.SET_NULL, to='menu.menuitem'),
),
migrations.AlterField(
model_name='menuitem',
name='rank',
field=models.IntegerField(default=1000, help_text='Order of the menu. Lowest First.'),
),
migrations.AlterField(
model_name='menuitem',
name='text',
field=models.CharField(blank=True, default=None, help_text='Text to show on menu', max_length=150, null=True),
),
]

View File

@ -1,174 +1,132 @@
import logging
from allianceauth.hooks import get_hooks
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
from allianceauth.menu.constants import DEFAULT_FOLDER_ICON_CLASSES
from .constants import DEFAULT_MENU_ITEM_ORDER, MenuItemType
from .core.menu_item_hooks import MenuItemHookCustom
from .managers import MenuItemManager
class MenuItem(models.Model):
# Auto Generated model from an auth_hook
hook_function = models.CharField(
max_length=500, default=None, null=True, blank=True)
"""An item in the sidebar menu.
Some of these objects are generated from `MenuItemHook` objects.
To avoid confusion we are using the same same field names.user defined
"""
# User Made Model
icon_classes = models.CharField(
max_length=150, default=None, null=True, blank=True, help_text="Font Awesome classes to show as icon on menu")
text = models.CharField(
max_length=150, default=None, null=True, blank=True, help_text="Text to show on menu")
url = models.CharField(max_length=2048, default=None,
null=True, blank=True)
# Put it under a header?
max_length=150,
db_index=True,
verbose_name=_("text"),
help_text=_("Text to show on menu"),
)
order = models.IntegerField(
default=DEFAULT_MENU_ITEM_ORDER,
db_index=True,
verbose_name=_("order"),
help_text=_("Order of the menu. Lowest First"),
)
parent = models.ForeignKey(
'self', on_delete=models.SET_NULL, null=True, blank=True, help_text="Parent Header. (Optional)")
"self",
on_delete=models.SET_NULL,
default=None,
null=True,
blank=True,
related_name="children",
verbose_name=_("folder"),
help_text=_("Folder this item is in (optional)"),
)
is_hidden = models.BooleanField(
default=False,
verbose_name=_("is hidden"),
help_text=_(
"Hide this menu item."
"If this item is a folder all items under it will be hidden too"
),
)
# Put it where? lowest first
rank = models.IntegerField(default=1000, help_text="Order of the menu. Lowest First.")
# app related properties
hook_hash = models.CharField(
max_length=64, default=None, null=True, unique=True, editable=False
) # hash of a menu item hook. Must be nullable for unique comparison.
# Hide it fully? Hiding a parent will hide all it's children
hide = models.BooleanField(default=False, help_text="Hide this menu item. If this item is a header all items under it will be hidden too.")
# user defined properties
classes = models.CharField(
max_length=150,
default="",
blank=True,
verbose_name=_("icon classes"),
help_text=_(
"Font Awesome classes to show as icon on menu, "
"e.g. <code>fa-solid fa-house</code>"
),
)
url = models.TextField(
default="",
verbose_name=_("url"),
help_text=_("External URL this menu items will link to"),
)
class Meta:
indexes = [
models.Index(fields=['rank', ]),
]
objects = MenuItemManager()
def __str__(self) -> str:
return self.text
def save(self, *args, **kwargs):
if not self.hook_hash:
self.hook_hash = None # empty strings can create problems
return super().save(*args, **kwargs)
@property
def classes(self): # Helper function to make this model closer to the hook functions
return self.icon_classes
def item_type(self) -> MenuItemType:
"""Return the type of this menu item."""
if self.hook_hash:
return MenuItemType.APP
@staticmethod
def hook_to_name(mh):
return f"{mh.__class__.__module__}.{mh.__class__.__name__}"
if not self.url:
return MenuItemType.FOLDER
@staticmethod
def sync_hook_models():
# TODO define aa way for hooks to predefine a "parent" to create a sub menu from modules
menu_hooks = get_hooks('menu_item_hook')
hook_functions = []
for hook in menu_hooks:
mh = hook()
cls = MenuItem.hook_to_name(mh)
try:
# if it exists update the text only
# Users can adjust ranks so lets not change it if they have.
mi = MenuItem.objects.get(hook_function=cls)
mi.text = getattr(mh, "text", mh.__class__.__name__)
mi.save()
except MenuItem.DoesNotExist:
# This is a new hook, Make the database model.
MenuItem.objects.create(
hook_function=cls,
rank=getattr(mh, "order", 500),
text=getattr(mh, "text", mh.__class__.__name__)
)
hook_functions.append(cls)
return MenuItemType.LINK
# Get rid of any legacy hooks from modules removed
MenuItem.objects.filter(hook_function__isnull=False).exclude(
hook_function__in=hook_functions).delete()
@property
def is_app_item(self) -> bool:
"""Return True if this is an app item, else False."""
return self.item_type is MenuItemType.APP
@classmethod
def filter_items(cls, menu_item: dict):
"""
filter any items with no valid children from a menu
"""
count_items = len(menu_item['items'])
if count_items: # if we have children confirm we can see them
for i in menu_item['items']:
if len(i['render']) == 0:
count_items -= 1
if count_items == 0: # no children left dont render header
return False
return True
else:
return True
@property
def is_child(self) -> bool:
"""Return True if this item is a child, else False."""
return bool(self.parent_id)
@classmethod
def render_menu(cls, request):
"""
Return the sorted side menu items with any items the user can't see removed.
"""
# Override all the items to the bs5 theme
template = "menu/menu-item-bs5.html"
# TODO discuss permissions for user defined links
@property
def is_folder(self) -> bool:
"""Return True if this item is a folder, else False."""
return self.item_type is MenuItemType.FOLDER
# Turn all the hooks into functions
menu_hooks = get_hooks('menu_item_hook')
items = {}
for fn in menu_hooks:
f = fn()
items[cls.hook_to_name(f)] = f
@property
def is_link_item(self) -> bool:
"""Return True if this item is a link item, else False."""
return self.item_type is MenuItemType.LINK
menu_items = MenuItem.objects.all().order_by("rank")
@property
def is_user_defined(self) -> bool:
"""Return True if this item is user defined."""
return self.item_type is not MenuItemType.APP
menu = {}
for mi in menu_items:
if mi.hide:
# hidden item, skip it completely
continue
try:
_cnt = 0
_render = None
if mi.hook_function:
# This is a module hook, so we need to render it as the developer intended
# TODO add a new attribute for apps that want to override it in the new theme
items[mi.hook_function].template = template
_render = items[mi.hook_function].render(request)
_cnt = items[mi.hook_function].count
else:
# This is a user defined menu item so we render it with defaults.
_render = render_to_string(template,
{'item': mi},
request=request)
def to_hook_obj(self) -> MenuItemHookCustom:
"""Convert to hook object for rendering."""
if self.is_app_item:
raise ValueError("The related hook objects should be used for app items.")
parent = mi.id
if mi.parent_id: # Set it if present
parent = mi.parent_id
hook_obj = MenuItemHookCustom(
text=self.text, classes=self.classes, url_name="", order=self.order
)
hook_obj.navactive = []
if self.is_folder and not self.classes:
hook_obj.classes = DEFAULT_FOLDER_ICON_CLASSES
if parent not in menu: # this will cause the menu headers to be out of order
menu[parent] = {"items": [],
"count": 0,
"render": None,
"text": "None",
"rank": 9999,
}
_mi = {
"count": _cnt,
"render": _render,
"text": mi.text,
"rank": mi.rank,
"classes": (mi.icon_classes if mi.icon_classes != "" else "fa-solid fa-folder"),
"hide": mi.hide
}
if parent != mi.id:
# this is a sub item
menu[parent]["items"].append(_mi)
if _cnt:
#add its count to the header count
menu[parent]["count"] += _cnt
else:
if len(menu[parent]["items"]):
# this is a top folder dont update the count.
del(_mi["count"])
menu[parent].update(_mi)
except Exception as e:
logger.exception(e)
# reset to list
menu = list(menu.values())
# sort the menu list as the parents may be out of order.
menu.sort(key=lambda i: i['rank'])
# ensure no empty groups
menu = filter(cls.filter_items, menu)
return menu
hook_obj.url = self.url
hook_obj.is_folder = self.is_folder
hook_obj.html_id = f"id-folder-{self.id}" if self.is_folder else ""
return hook_obj

View File

@ -1,49 +0,0 @@
import logging
from django.core.cache import cache
from allianceauth.menu.models import MenuItem
from allianceauth.utils.django import StartupCommand
logger = logging.getLogger(__name__)
MENU_SYNC_CACHE_KEY = "ALLIANCEAUTH-MENU-SYNCED"
MENU_CACHE_KEY = "ALLIANCEAUTH-MENU-CACHE"
class MenuProvider():
def clear_synced_flag(self) -> bool:
return cache.delete(MENU_SYNC_CACHE_KEY)
def set_synced_flag(self) -> bool:
return cache.set(MENU_SYNC_CACHE_KEY, True)
def get_synced_flag(self) -> bool:
return cache.get(MENU_SYNC_CACHE_KEY, False)
def sync_menu_models(self):
MenuItem.sync_hook_models()
self.set_synced_flag()
def check_and_sync_menu(self) -> None:
if self.get_synced_flag():
# performance hit to each page view to ensure tests work.
# tests clear DB but not cache.
# TODO rethink all of this?
if MenuItem.objects.all().count() > 0:
logger.debug("Menu Hooks Synced")
else:
self.sync_menu_models()
else:
logger.debug("Syncing Menu Hooks")
self.sync_menu_models()
def get_and_cache_menu(self):
pass
def clear_menu_cache(self):
pass
menu_provider = MenuProvider()

View File

@ -0,0 +1,13 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a href="{% url 'admin:menu_menuitem_add' %}?type={{ folder_type }}" class="addlink">
{% translate "Add folder" %}
</a>
</li>
{% endblock %}

View File

@ -1,7 +1,3 @@
{% for data in menu_items %}
{% if data.items|length > 0 %}
{% include "menu/menu-item-bs5.html" with item=data %}
{% else %}
{{ data.render }}
{% endif %}
{% for item in menu_items %}
{{ item.html }}
{% endfor %}

View File

@ -1,37 +1,57 @@
{% load i18n %}
{% load navactive %}
{% if not item.hide %}
<li class="d-flex flex-wrap m-2 p-2 pt-0 pb-0 mt-0 mb-0 me-0 pe-0">
<i class="nav-link {{ item.classes }} fa-fw align-self-center me-3 {% if item.navactive %}{% navactive request item.navactive|join:' ' %}{% endif %}" {% if item.items|length %} type="button" data-bs-toggle="collapse" data-bs-target="#id-{{ item.text|slugify }}" aria-expanded="false" aria-controls="" {% endif %}></i>
<a class="nav-link flex-fill align-self-center" {% if item.items|length %} type="button" data-bs-toggle="collapse" data-bs-target="#id-{{ item.text|slugify }}" aria-expanded="false" aria-controls="" {% endif %}
href="{% if item.url_name %}{% url item.url_name %}{% else %}{{ item.url }}{% endif %}">
{% translate item.text %}
</a>
{% if item.count >= 1 %}
<span class="badge bg-primary m-2 align-self-center {% if item.items|length == 0 %}me-4{% endif %}">
{{ item.count }}
</span>
{% elif item.url %}
<span class="pill m-2 me-4 align-self-center fas fa-external-link-alt"></span>
<li class="d-flex flex-wrap m-2 p-2 pt-0 pb-0 mt-0 mb-0 me-0 pe-0">
<i
class="nav-link {{ item.classes }} fa-fw align-self-center me-3 {% if item.navactive %}{% navactive request item.navactive|join:' ' %}{% endif %}"
{% if item.is_folder %}
type="button"
data-bs-toggle="collapse"
data-bs-target="#{{ item.html_id }}"
aria-expanded="false"
aria-controls=""
{% endif %}>
</i>
<a
class="nav-link flex-fill align-self-center me-auto"
{% if item.is_folder %}
type="button"
data-bs-toggle="collapse"
data-bs-target="#{{ item.html_id }}"
aria-expanded="false"
aria-controls=""
{% endif %}
href="{% if item.url_name %}{% url item.url_name %}{% else %}{{ item.url }}{% endif %}">
{% translate item.text %}
</a>
{% if item.items|length > 0 %}
<span
class="pill m-2 me-4 align-self-center fas fa-solid fa-chevron-down"
type="button"
data-bs-toggle="collapse"
data-bs-target="#id-{{ item.text|slugify }}"
aria-expanded="false"
aria-controls="">
</span>
<!--<hr class="m-0 w-100">-->
<ul class="collapse ps-1 w-100 border-start rounded-start border-light border-3" id="id-{{ item.text|slugify }}">
{% for sub_item in item.items %}
{{ sub_item.render }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endif %}
{% if item.count >= 1 %}
<span class="badge bg-primary m-2 align-self-center{% if not item.is_folder %} me-2{% endif %}">
{{ item.count }}
</span>
{% elif item.url %}
<span class="pill m-2 me-4 align-self-center fas fa-external-link-alt"></span>
{% endif %}
{% if item.is_folder %}
<span
class="pill m-2 align-self-center collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#{{ item.html_id }}"
aria-expanded="false"
aria-controls=""
>
<i class="fas fa-chevron-right"></i>
<i class="fas fa-chevron-down"></i>
</span>
<ul
class="collapse ps-1 w-100 border-start rounded-start border-light border-3"
id="{{ item.html_id }}">
{% for sub_item in item.children %}
{{ sub_item }}
{% endfor %}
</ul>
{% endif %}
</li>

View File

@ -0,0 +1,3 @@
<div class="align-items-center text-center">
{% include "bundles/image-auth-logo.html" %}
</div>

View File

@ -2,57 +2,89 @@
{% load evelinks %}
{% load theme_tags %}
<div style="z-index:5;" class="w100 d-flex flex-column justify-content-center align-items-center text-center pb-2">
{% if user.is_authenticated %}
{% if request.user.profile.main_character %}
{% with request.user.profile.main_character as main %}
<div class="p-2 position-relative m-2">
<div id="aa-user-info" class="w-100 d-flex flex-column justify-content-center align-items-center text-center py-1 border-top border-secondary {% if not user.is_authenticated %}position-absolute bottom-0{% endif %}">
<div class="d-flex mb-0 w-100">
<div class="p-2 position-relative m-2">
{% if user.is_authenticated %}
{% with request.user.profile.main_character as main %}
<img class="rounded-circle" src="{{ main.character_id|character_portrait_url:64 }}" alt="{{ main.character_name }}">
<img class="rounded-circle position-absolute bottom-0 start-0" src="{{ main.corporation_logo_url_32 }}" alt="{{ main.corporation_name }}">
{% if main.alliance_id %}
<img class="rounded-circle position-absolute bottom-0 end-0" src="{{ main.alliance_logo_url_32 }}" alt="{{ main.alliance_name }}">
{% elif main.faction_id %}
<img class="rounded-circle position-absolute bottom-0 end-0" src="{{ main.faction_logo_url_32 }}" alt="{{ main.faction_name }}">
{% endif %}
</div>
<h5>{{ main.character_name }}</h5>
{% endwith %}
{% else %}
<img class="rounded-circle m-2" src="{{ 1|character_portrait_url:32 }}" alt="{% translate 'No Main Character!' %}">
<h5>{% translate "No Main Character!" %}</h5>
{% endif %}
{% endwith %}
{% else %}
{% include "bundles/image-auth-logo.html" with logo_width="64px" %}
{% endif %}
</div>
<div class="align-self-center text-start">
{% if user.is_authenticated %}
{% with request.user.profile.main_character as main %}
<h5 class="m-0">{{ main.character_name }}</h5>
<p class="m-0 small">{{ main.corporation_name }}</p>
{% if main.alliance_id %}
<p class="m-0 small">{{ main.alliance_name }}</p>
{% elif main.faction_id %}
<p class="m-0 small">{{ main.faction_name }}</p>
{% endif %}
{% endwith %}
{% else %}
<h5 class="m-0">{{ SITE_NAME }}</h5>
{% endif %}
</div>
<div class="ms-auto dropup">
<button type="button" class="h-100 btn" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-gear fa-fw text-light"></i>
</button>
<ul class="dropdown-menu" style="min-width: 200px;">
<li><h6 class="dropdown-header">{% translate "Language" %}</h6></li>
<li>
<a class="dropdown-item">{% include "public/lang_select.html" %}</a>
</li>
<li><h6 class="dropdown-header">{% translate "Theme" %}</h6></li>
{% theme_select %}
{% endif %}
<li>
<a class="dropdown-item">
{% theme_select %}
</a>
</li>
<div class="btn-group m-2">
<button type="button" class="btn btn-secondary p-1">
{% include "public/lang_select.html" %}
</button>
{% if user.is_superuser %}
<a role="button" class="btn btn btn-secondary d-flex" href="{% url 'admin:index' %}">
<span class="align-self-center">{% translate "Admin" %}</span>
</a>
{% endif %}
</div>
<div class="btn-group m-2">
{% if user.is_authenticated %}
<a role="button" class="btn btn-info" href="{% url 'authentication:token_management' %}" title="Token Management"><i class="fa-solid fa-user-lock fa-fw"></i></a>
{% endif %}
{% if user.is_superuser %}
<a role="button" class="btn btn-info" href="https://allianceauth.readthedocs.io/" title="Alliance Auth Documentation"><i class="fa-solid fa-book fa-fw"></i></a>
<a role="button" class="btn btn-info" href="https://discord.gg/fjnHAmk" title="Alliance Auth Discord"><i class="fa-brands fa-discord fa-fw"></i></a>
<a role="button" class="btn btn-info" href="https://gitlab.com/allianceauth/allianceauth" title="Alliance Auth Git"><i class="fa-brands fa-gitlab fa-fw"></i></a>
{% endif %}
{% if user.is_authenticated %}
<a role="button" class="btn btn-danger" href="{% url 'logout' %}" title="{% translate 'Sign Out' %}"><i class="fa-solid fa-right-from-bracket fa-fw"></i></a>
{% else %}
<a role="button" class="btn btn-success" href="{% url 'authentication:login' %}" title="{% translate 'Sign In' %}"> <i class="fa-solid fa-right-to-bracket fa-fw"></i></a>
{% endif %}
{% if user.is_superuser %}
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">{% translate "Super User" %}</h6></li>
<li>
<a class="dropdown-item" href="https://allianceauth.readthedocs.io/" title="Alliance Auth Documentation"><i class="fa-solid fa-book fa-fw"></i> Alliance Auth Documentation</a>
</li>
<li>
<a class="dropdown-item" href="https://discord.gg/fjnHAmk" title="Alliance Auth Discord"><i class="fa-brands fa-discord fa-fw"></i> Alliance Auth Discord</a>
</li>
<li>
<a class="dropdown-item" href="https://gitlab.com/allianceauth/allianceauth" title="Alliance Auth Git"><i class="fa-brands fa-gitlab fa-fw"></i> Alliance Auth Git</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'admin:index' %}">
<i class="fa-solid fa-gear fa-fw"></i> {% translate "Admin" %}
</a>
</li>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% if user.is_authenticated %}
<li>
<a class="dropdown-item" href="{% url 'authentication:token_management' %}">
<i class="fa-solid fa-user-lock fa-fw"></i> Token Management
</a>
</li>
<li>
<a class="dropdown-item text-danger" href="{% url 'logout' %}" title="{% translate 'Sign Out' %}"><i class="fa-solid fa-right-from-bracket fa-fw "></i> {% translate 'Sign Out' %}</a>
</li>
{% else %}
<li>
<a class="dropdown-item text-success" href="{% url 'authentication:login' %}" title="{% translate 'Sign In' %}"> <i class="fa-solid fa-right-to-bracket fa-fw "></i> {% translate 'Sign In' %}</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>

View File

@ -4,10 +4,10 @@
<div class="col-auto px-0">
<div class="collapse collapse-horizontal" tabindex="-1" id="sidebar">
<div style="width: 350px;">
<div class="nav-padding navbar-dark bg-dark text-light px-0 d-flex flex-column overflow-hidden vh-100 auth-logo">
<div>
<div class="nav-padding navbar-dark bg-dark text-light px-0 d-flex flex-column overflow-hidden vh-100 {% if not user.is_authenticated %}position-relative{% endif %}">
{% if user.is_authenticated %}
<ul style="z-index:5;" id="sidebar-menu" class="navbar-nav flex-column mb-auto overflow-auto pt-2">
<ul id="sidebar-menu" class="navbar-nav flex-column mb-auto overflow-auto pt-2">
<li class="d-flex flex-wrap m-2 p-2 pt-0 pb-0 mt-0 mb-0 me-0 pe-0">
<i class="nav-link fas fa-tachometer-alt fa-fw align-self-center me-3 {% navactive request 'authentication:dashboard' %}"></i>
<a class="nav-link flex-fill align-self-center" href="{% url 'authentication:dashboard' %}">
@ -15,8 +15,10 @@
</a>
</li>
{% sorted_menu_items %}
{% menu_items %}
</ul>
{% include 'menu/menu-logo.html' %}
{% endif %}
{% include 'menu/menu-user.html' %}

View File

@ -0,0 +1,32 @@
"""Template tags for rendering the classic side menu."""
from django import template
from django.http import HttpRequest
from allianceauth.hooks import get_hooks
register = template.Library()
# TODO: Show user created menu items
# TODO: Apply is_hidden feature for BS3 type items
@register.inclusion_tag("public/menublock.html", takes_context=True)
def menu_items(context: dict) -> dict:
"""Render menu items for classic dashboard."""
items = render_menu(context["request"])
return {"menu_items": items}
def render_menu(request: HttpRequest):
"""Return the rendered side menu for including in a template.
This function is creating a BS3 style menu.
"""
hooks = get_hooks("menu_item_hook")
raw_items = [fn() for fn in hooks]
raw_items.sort(key=lambda i: i.order)
menu_items = [item.render(request) for item in raw_items]
return menu_items

View File

@ -1,34 +1,174 @@
"""Template tags for rendering the new side menu.
Documentation of the render logic
---------------------------------
The are 3 types of menu items:
- App entries: Generated by hooks from Django apps
- Link entries: Linking to external pages. User created.
- Folder: Grouping together several app or link entries. User created.
The MenuItem model holds the current list of all menu items.
App entries are linked to a `MenuItemHook` object in the respective Django app.
Those hook objects contain dynamic logic in a `render()` method,
which must be executed when rendering for the current request.
Since the same template must be used to render all items, link entries and folders
are converted to `MenuItemHookCustom` objects, a sub class of `MenuItemHook`.
This ensures the template only rendered objects of one specific type or sub-type.
The rendered menu items are finally collected in a list of RenderedMenuItem objects,
which is used to render the complete menu.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from django import template
from django.db.models import QuerySet
from django.http import HttpRequest
from allianceauth.hooks import get_hooks
from allianceauth.menu.core import menu_item_hooks, smart_sync
from allianceauth.menu.models import MenuItem
from allianceauth.services.auth_hooks import MenuItemHook
register = template.Library()
def process_menu_items(hooks, request):
_menu_items = list()
items = [fn() for fn in hooks]
items.sort(key=lambda i: i.order)
for item in items:
_menu_items.append(item.render(request))
return _menu_items
@register.inclusion_tag("menu/menu-block.html", takes_context=True)
def menu_items(context: dict) -> dict:
"""Render menu items for new dashboards."""
smart_sync.sync_menu()
items = render_menu(context["request"])
return {"menu_items": items}
@register.inclusion_tag('public/menublock.html', takes_context=True)
def menu_items(context):
request = context['request']
@dataclass
class RenderedMenuItem:
"""A rendered menu item.
return {
'menu_items': process_menu_items(get_hooks('menu_item_hook'), request),
}
These objects can be rendered with the menu-block template.
"""
menu_item: MenuItem
children: List["RenderedMenuItem"] = field(default_factory=list)
count: Optional[int] = None
html: str = ""
html_id: str = ""
@property
def is_folder(self) -> bool:
"""Return True if this item is a folder."""
return self.menu_item.is_folder
def update_html(self, request: HttpRequest, template: str):
"""Render this menu item with defaults and set HTML ID."""
hook_obj = self.menu_item.to_hook_obj()
hook_obj.template = template
hook_obj.count = self.count
if self.is_folder:
hook_obj.children = [child.html for child in self.children]
self.html = hook_obj.render(request)
self.html_id = hook_obj.html_id
@register.inclusion_tag('menu/menu-block.html', takes_context=True)
def sorted_menu_items(context):
request = context['request']
menu_items = MenuItem.render_menu(request)
return {
'menu_items':menu_items
}
def render_menu(request: HttpRequest) -> List[RenderedMenuItem]:
"""Return the rendered side menu for including in a template.
This function is creating BS5 style menus.
"""
hook_items = _gather_menu_items_from_hooks()
# Menu items needs to be rendered with the new BS5 template
bs5_template = "menu/menu-item-bs5.html"
rendered_items: Dict[int, RenderedMenuItem] = {}
menu_items: QuerySet[MenuItem] = MenuItem.objects.order_by(
"parent", "order", "text"
)
for item in menu_items:
if item.is_hidden:
continue # do not render hidden items
if item.is_app_item:
rendered_item = _render_app_item(request, hook_items, item, bs5_template)
elif item.is_link_item:
rendered_item = _render_link_item(request, item, bs5_template)
elif item.is_folder:
rendered_item = RenderedMenuItem(item) # we render these items later
else:
raise NotImplementedError("Unknown menu item type")
if item.is_child:
try:
parent = rendered_items[item.parent_id]
except KeyError:
continue # do not render children of hidden folders
parent.children.append(rendered_item)
if rendered_item.count is not None:
if parent.count is None:
parent.count = 0
parent.count += rendered_item.count
else:
rendered_items[item.id] = rendered_item
_remove_empty_folders(rendered_items)
_render_folder_items(request, rendered_items, bs5_template)
return list(rendered_items.values())
def _gather_menu_items_from_hooks() -> Dict[str, MenuItemHook]:
hook_items = {}
for hook in get_hooks("menu_item_hook"):
f = hook()
hook_items[menu_item_hooks.generate_hash(f)] = f
return hook_items
def _render_app_item(
request: HttpRequest, hook_items: dict, item: MenuItem, new_template: str
) -> RenderedMenuItem:
# This is a module hook, so we need to render it as the developer intended
# TODO add a new attribute for apps that want to override it in the new theme
hook_item = hook_items[item.hook_hash]
hook_item.template = new_template
html = hook_item.render(request)
count = hook_item.count
rendered_item = RenderedMenuItem(menu_item=item, count=count, html=html)
return rendered_item
def _render_link_item(
request: HttpRequest, item: MenuItem, new_template: str
) -> RenderedMenuItem:
rendered_item = RenderedMenuItem(menu_item=item)
rendered_item.update_html(request, template=new_template)
return rendered_item
def _render_folder_items(
request: HttpRequest, rendered_items: Dict[int, RenderedMenuItem], new_template: str
):
for item in rendered_items.values():
if item.menu_item.is_folder:
item.update_html(request=request, template=new_template)
def _remove_empty_folders(rendered_items: Dict[int, RenderedMenuItem]):
ids_to_remove = []
for item_id, item in rendered_items.items():
if item.is_folder and not item.children:
ids_to_remove.append(item_id)
for item_id in ids_to_remove:
del rendered_items[item_id]

View File

View File

View File

@ -0,0 +1,63 @@
from django.test import TestCase
from allianceauth.menu.core.menu_item_hooks import (
MenuItemHookCustom,
gather_params,
generate_hash,
)
from allianceauth.menu.tests.factories import create_menu_item_hook_function
class TestGenerateHash(TestCase):
def test_should_generate_same_hash(self):
# given
hook = create_menu_item_hook_function()
# when
result_1 = generate_hash(hook())
result_2 = generate_hash(hook())
# then
self.assertIsInstance(result_1, str)
self.assertEqual(result_1, result_2)
def test_should_generate_different_hashes(self):
# given
hook_1 = create_menu_item_hook_function()
hook_2 = create_menu_item_hook_function()
# when
result_1 = generate_hash(hook_1())
result_2 = generate_hash(hook_2())
# then
self.assertNotEqual(result_1, result_2)
class TestExtractParams(TestCase):
def test_should_return_params(self):
# given
hook = create_menu_item_hook_function(text="Alpha", order=42)
# when
result = gather_params(hook())
# then
self.assertEqual(result.text, "Alpha")
self.assertEqual(result.order, 42)
self.assertIsInstance(result.hash, str)
class TestMenuItemHookCustom(TestCase):
def test_should_create_minimal(self):
# when
obj = MenuItemHookCustom(text="text", classes="classes", url_name="url_name")
# then
self.assertEqual(obj.text, "text")
self.assertEqual(obj.classes, "classes")
self.assertEqual(obj.url_name, "url_name")
self.assertEqual(obj.url, "")
self.assertIsNone(obj.is_folder)
self.assertEqual(obj.html_id, "")
self.assertListEqual(obj.children, [])

View File

@ -0,0 +1,42 @@
from unittest.mock import patch
from django.test import TestCase
from allianceauth.menu.core import smart_sync
from allianceauth.menu.tests.factories import create_link_menu_item
from allianceauth.menu.tests.utils import PACKAGE_PATH
@patch(PACKAGE_PATH + ".models.MenuItem.objects.sync_all", spec=True)
class TestSmartSync(TestCase):
def test_should_sync_after_reset(self, mock_sync_all):
# given
smart_sync.reset_menu_items_sync()
mock_sync_all.reset_mock()
# when
smart_sync.sync_menu()
# then
self.assertTrue(mock_sync_all.called)
def test_should_sync_when_sync_flag_is_set_but_no_items_in_db(self, mock_sync_all):
# given
smart_sync._record_menu_was_synced()
# when
smart_sync.sync_menu()
# then
self.assertTrue(mock_sync_all.called)
def test_should_not_sync_when_sync_flag_is_set_and_items_in_db(self, mock_sync_all):
# given
smart_sync._record_menu_was_synced()
create_link_menu_item()
# when
smart_sync.sync_menu()
# then
self.assertFalse(mock_sync_all.called)

View File

@ -0,0 +1,95 @@
from itertools import count
from django.contrib.auth.models import User
from allianceauth.menu.core import menu_item_hooks
from allianceauth.menu.models import MenuItem
from allianceauth.menu.templatetags.menu_menu_items import RenderedMenuItem
from allianceauth.services.auth_hooks import MenuItemHook
from allianceauth.tests.auth_utils import AuthUtils
def create_user(permissions=None, **kwargs) -> User:
num = next(counter_user)
params = {"username": f"test_user_{num}"}
params.update(kwargs)
user = User.objects.create(**params)
if permissions:
user = AuthUtils.add_permissions_to_user_by_name(perms=permissions, user=user)
return user
def create_menu_item_hook(**kwargs) -> MenuItemHook:
num = next(counter_menu_item_hook)
new_class = type(f"GeneratedMenuItem{num}", (MenuItemHook,), {})
count = kwargs.pop("count", None)
params = {
"text": f"Dummy App #{num}",
"classes": "fa-solid fa-users-gear",
"url_name": "groupmanagement:management",
}
params.update(kwargs)
obj = new_class(**params)
for key, value in params.items():
setattr(obj, key, value)
obj.count = count
return obj
def create_menu_item_hook_function(**kwargs):
obj = create_menu_item_hook(**kwargs)
return lambda: obj
def create_link_menu_item(**kwargs) -> MenuItem:
num = next(counter_menu_item)
params = {
"url": f"https://www.example.com/{num}",
}
params.update(kwargs)
return _create_menu_item(**params)
def create_app_menu_item(**kwargs) -> MenuItem:
params = {"hook_hash": "hook_hash"}
params.update(kwargs)
return _create_menu_item(**params)
def create_folder_menu_item(**kwargs) -> MenuItem:
return _create_menu_item(**kwargs)
def create_menu_item_from_hook(hook, **kwargs) -> MenuItem:
item = hook()
hook_params = menu_item_hooks.gather_params(item)
params = {
"text": hook_params.text,
"hook_hash": hook_params.hash,
"order": hook_params.order,
}
params.update(kwargs)
return _create_menu_item(**params)
def _create_menu_item(**kwargs) -> MenuItem:
num = next(counter_menu_item)
params = {
"text": f"text #{num}",
}
params.update(kwargs)
return MenuItem.objects.create(**params)
def create_rendered_menu_item(**kwargs) -> RenderedMenuItem:
if "menu_item" not in kwargs:
kwargs["menu_item"] = create_link_menu_item()
return RenderedMenuItem(**kwargs)
counter_menu_item = count(1, 1)
counter_menu_item_hook = count(1, 1)
counter_user = count(1, 1)

View File

@ -0,0 +1,178 @@
from http import HTTPStatus
from django.test import TestCase
from django.urls import reverse
from allianceauth.menu.constants import MenuItemType
from allianceauth.menu.forms import (
AppMenuItemAdminForm,
FolderMenuItemAdminForm,
LinkMenuItemAdminForm,
)
from allianceauth.menu.models import MenuItem
from allianceauth.menu.tests.factories import (
create_app_menu_item,
create_folder_menu_item,
create_link_menu_item,
create_user,
)
from allianceauth.menu.tests.utils import extract_html
def extract_menu_item_texts(response):
"""Extract labels of menu items shown in change list."""
soup = extract_html(response)
items = soup.find_all("th", {"class": "field-_text"})
labels = {elem.text for elem in items}
return labels
class TestAdminSite(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.user = create_user(is_superuser=True, is_staff=True)
cls.changelist_url = reverse("admin:menu_menuitem_changelist")
cls.add_url = reverse("admin:menu_menuitem_add")
def change_url(self, id_):
return reverse("admin:menu_menuitem_change", args=[id_])
def test_changelist_should_show_all_types(self):
# given
self.client.force_login(self.user)
create_app_menu_item(text="app")
create_folder_menu_item(text="folder")
create_link_menu_item(text="link")
# when
response = self.client.get(self.changelist_url)
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
labels = extract_menu_item_texts(response)
self.assertSetEqual(labels, {"app", "[folder]", "link"})
def test_should_create_new_link_item(self):
# given
self.client.force_login(self.user)
# when
response = self.client.post(
self.add_url,
{"text": "alpha", "url": "http://www.example.com", "order": 99},
)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.text, "alpha")
self.assertEqual(obj.item_type, MenuItemType.LINK)
def test_should_create_new_folder_item(self):
# given
self.client.force_login(self.user)
# when
response = self.client.post(
self.add_url + "?type=folder", {"text": "alpha", "order": 99}
)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.text, "alpha")
self.assertEqual(obj.item_type, MenuItemType.FOLDER)
def test_should_change_app_item(self):
# given
self.client.force_login(self.user)
item = create_app_menu_item(text="alpha", order=1)
form_data = AppMenuItemAdminForm(instance=item).initial
form_data["order"] = 99
form_data["parent"] = ""
# when
response = self.client.post(self.change_url(item.id), form_data)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.order, 99)
self.assertEqual(obj.item_type, MenuItemType.APP)
def test_should_change_link_item(self):
# given
self.client.force_login(self.user)
item = create_link_menu_item(text="alpha")
form_data = LinkMenuItemAdminForm(instance=item).initial
form_data["text"] = "bravo"
form_data["parent"] = ""
# when
response = self.client.post(self.change_url(item.id), form_data)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.text, "bravo")
self.assertEqual(obj.item_type, MenuItemType.LINK)
def test_should_change_folder_item(self):
# given
self.client.force_login(self.user)
item = create_folder_menu_item(text="alpha")
form_data = FolderMenuItemAdminForm(instance=item).initial
form_data["text"] = "bravo"
# when
response = self.client.post(self.change_url(item.id), form_data)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.text, "bravo")
self.assertEqual(obj.item_type, MenuItemType.FOLDER)
def test_should_move_item_into_folder(self):
# given
self.client.force_login(self.user)
link = create_link_menu_item(text="alpha")
folder = create_folder_menu_item(text="folder")
form_data = LinkMenuItemAdminForm(instance=link).initial
form_data["parent"] = folder.id
# when
response = self.client.post(self.change_url(link.id), form_data)
# then
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, self.changelist_url)
link.refresh_from_db()
self.assertEqual(link.parent, folder)
def test_should_filter_items_by_type(self):
# given
self.client.force_login(self.user)
create_app_menu_item(text="app")
create_folder_menu_item(text="folder")
create_link_menu_item(text="link")
# when
cases = [("link", "link"), ("app", "app"), ("folder", "[folder]")]
for filter_name, expected_label in cases:
with self.subTest(filter_name=filter_name):
response = self.client.get(self.changelist_url + f"?type={filter_name}")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
labels = extract_menu_item_texts(response)
self.assertSetEqual(labels, {expected_label})

View File

@ -0,0 +1,102 @@
from http import HTTPStatus
from django.test import TestCase
from allianceauth.menu.core.smart_sync import reset_menu_items_sync
from allianceauth.menu.tests.factories import (
create_folder_menu_item,
create_link_menu_item,
create_user,
)
from allianceauth.menu.tests.utils import extract_links
class TestDefaultDashboardWithSideMenu(TestCase):
def test_should_show_all_types_of_menu_entries(self):
# given
user = create_user(permissions=["auth.group_management"])
self.client.force_login(user)
create_link_menu_item(text="Alpha", url="http://www.example.com/alpha")
folder = create_folder_menu_item(text="Folder")
create_link_menu_item(
text="Bravo", url="http://www.example.com/bravo", parent=folder
)
reset_menu_items_sync() # this simulates startup
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
links = extract_links(response)
# open_page_in_browser(response)
self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups")
self.assertEqual(links["/groupmanagement/requests/"], "Group Management")
self.assertEqual(links["http://www.example.com/alpha"], "Alpha")
self.assertEqual(links["http://www.example.com/bravo"], "Bravo")
def test_should_not_show_menu_entry_when_user_has_no_permission(self):
# given
user = create_user()
self.client.force_login(user)
reset_menu_items_sync()
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
links = extract_links(response)
self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups")
self.assertNotIn("/groupmanagement/requests/", links)
def test_should_not_show_menu_entry_when_hidden(self):
# given
user = create_user()
self.client.force_login(user)
create_link_menu_item(text="Alpha", url="http://www.example.com/")
reset_menu_items_sync()
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
links = extract_links(response)
self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups")
self.assertNotIn("http://www.example.com/alpha", links)
class TestBS3DashboardWithSideMenu(TestCase):
def test_should_not_show_group_management_when_user_has_no_permission(self):
# given
user = create_user()
self.client.force_login(user)
# when
response = self.client.get("/dashboard_bs3/")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
links = extract_links(response)
self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups")
self.assertNotIn("/groupmanagement/requests/", links)
def test_should_show_group_management_when_user_has_permission(self):
# given
user = create_user(permissions=["auth.group_management"])
self.client.force_login(user)
# when
response = self.client.get("/dashboard_bs3/")
# then
self.assertEqual(response.status_code, HTTPStatus.OK)
links = extract_links(response)
self.assertEqual(links["/dashboard/"], "Dashboard")
self.assertEqual(links["/groups/"], "Groups")
self.assertEqual(links["/groupmanagement/requests/"], "Group Management")

View File

@ -0,0 +1,54 @@
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from allianceauth.menu.templatetags.menu_items import render_menu
from allianceauth.menu.tests.factories import create_menu_item_hook_function
from allianceauth.menu.tests.utils import PACKAGE_PATH, render_template
MODULE_PATH = PACKAGE_PATH + ".templatetags.menu_items"
class TestTemplateTags(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
@patch(MODULE_PATH + ".render_menu", spec=True)
def test_menu_items(self, mock_render_menu):
# given
mock_render_menu.return_value = ["Alpha"]
request = self.factory.get("/")
# when
rendered = render_template(
"{% load menu_items %}{% menu_items %}",
context={"request": request},
)
self.assertIn("Alpha", rendered)
@patch(MODULE_PATH + ".get_hooks", spec=True)
class TestRenderMenu(TestCase):
def setUp(self) -> None:
self.factory = RequestFactory()
def test_should_render_menu_in_order(self, mock_get_hooks):
# given
mock_get_hooks.return_value = [
create_menu_item_hook_function(text="Charlie"),
create_menu_item_hook_function(text="Alpha", order=1),
create_menu_item_hook_function(text="Bravo", order=2),
]
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 3)
self.assertIn("Alpha", menu[0])
self.assertIn("Bravo", menu[1])
self.assertIn("Charlie", menu[2])

View File

@ -0,0 +1,326 @@
from typing import List, NamedTuple, Optional
from unittest.mock import patch
from bs4 import BeautifulSoup
from django.test import RequestFactory, TestCase
from allianceauth.menu.templatetags.menu_menu_items import (
RenderedMenuItem,
render_menu,
)
from allianceauth.menu.tests.factories import (
create_app_menu_item,
create_folder_menu_item,
create_link_menu_item,
create_menu_item_from_hook,
create_menu_item_hook_function,
create_rendered_menu_item,
)
from allianceauth.menu.tests.utils import (
PACKAGE_PATH,
remove_whitespaces,
render_template,
)
MODULE_PATH = PACKAGE_PATH + ".templatetags.menu_menu_items"
class TestTemplateTags(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
@patch(MODULE_PATH + ".render_menu", spec=True)
@patch(MODULE_PATH + ".smart_sync.sync_menu", spec=True)
def test_sorted_menu_items(self, mock_sync_menu, mock_render_menu):
# given
fake_item = {"html": "Alpha"}
mock_render_menu.return_value = [fake_item]
request = self.factory.get("/")
# when
rendered = render_template(
"{% load menu_menu_items %}{% menu_items %}",
context={"request": request},
)
self.assertIn("Alpha", rendered)
self.assertTrue(mock_sync_menu.called)
@patch(MODULE_PATH + ".get_hooks", spec=True)
class TestRenderDefaultMenu(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
def test_should_render_app_menu_items(self, mock_get_hooks):
# given
menu = [
create_menu_item_hook_function(text="Charlie", count=42),
create_menu_item_hook_function(text="Alpha", order=1),
create_menu_item_hook_function(text="Bravo", order=2),
]
mock_get_hooks.return_value = menu
for hook in menu:
create_menu_item_from_hook(hook)
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 3)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Bravo")
self.assertEqual(menu[2].menu_item.text, "Charlie")
self.assertEqual(menu[2].count, 42)
attrs = parse_html(menu[2])
self.assertEqual(attrs.count, 42)
self.assertEqual(attrs.text, "Charlie")
def test_should_render_link_menu_items(self, mock_get_hooks):
# given
mock_get_hooks.return_value = []
create_link_menu_item(text="Charlie"),
create_link_menu_item(text="Alpha", order=1),
create_link_menu_item(text="Bravo", order=2),
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 3)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Bravo")
self.assertEqual(menu[2].menu_item.text, "Charlie")
attrs = parse_html(menu[2])
self.assertEqual(attrs.text, "Charlie")
def test_should_render_folders(self, mock_get_hooks):
# given
mock_get_hooks.return_value = []
folder = create_folder_menu_item(text="Folder", order=2)
create_link_menu_item(text="Alpha", order=1)
create_link_menu_item(text="Bravo", order=3)
create_link_menu_item(text="Charlie", parent=folder)
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 3)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Folder")
self.assertEqual(menu[2].menu_item.text, "Bravo")
self.assertEqual(menu[1].children[0].menu_item.text, "Charlie")
attrs = parse_html(menu[1].children[0])
self.assertEqual(attrs.text, "Charlie")
def test_should_render_folder_properties(self, mock_get_hooks):
# given
# given
menu = [
create_menu_item_hook_function(text="Charlie", count=42),
create_menu_item_hook_function(text="Alpha", count=5),
create_menu_item_hook_function(text="Bravo"),
]
mock_get_hooks.return_value = menu
folder = create_folder_menu_item(text="Folder", order=1)
for hook in menu:
create_menu_item_from_hook(hook, parent=folder)
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 1)
item = menu[0]
self.assertEqual(item.menu_item.text, "Folder")
self.assertEqual(item.count, 47)
self.assertTrue(item.is_folder)
self.assertEqual(len(item.children), 3)
attrs = parse_html(item)
self.assertEqual(attrs.count, 47)
self.assertIn("fa-folder", attrs.classes)
def test_should_remove_empty_folders(self, mock_get_hooks):
# given
mock_get_hooks.return_value = []
create_folder_menu_item(text="Folder", order=2)
create_link_menu_item(text="Alpha", order=1)
create_link_menu_item(text="Bravo", order=3)
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 2)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Bravo")
def test_should_not_include_hidden_items(self, mock_get_hooks):
# given
mock_get_hooks.return_value = []
create_link_menu_item(text="Charlie"),
create_link_menu_item(text="Alpha", order=1),
create_link_menu_item(text="Bravo", order=2, is_hidden=True),
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 2)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Charlie")
def test_should_not_render_hidden_folders(self, mock_get_hooks):
# given
# given
menu = [
create_menu_item_hook_function(text="Charlie", count=42),
create_menu_item_hook_function(text="Alpha", count=5),
create_menu_item_hook_function(text="Bravo"),
]
mock_get_hooks.return_value = menu
folder = create_folder_menu_item(text="Folder", order=1, is_hidden=True)
for hook in menu:
create_menu_item_from_hook(hook, parent=folder)
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 0)
def test_should_allow_several_items_with_same_text(self, mock_get_hooks):
# given
mock_get_hooks.return_value = []
create_link_menu_item(text="Alpha", order=1),
create_link_menu_item(text="Alpha", order=2),
request = self.factory.get("/")
# when
result = render_menu(request)
# then
menu = list(result)
self.assertEqual(len(menu), 2)
self.assertEqual(menu[0].menu_item.text, "Alpha")
self.assertEqual(menu[1].menu_item.text, "Alpha")
class TestRenderedMenuItem(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
cls.template = "menu/menu-item-bs5.html"
def test_create_from_menu_item_with_defaults(self):
# given
item = create_link_menu_item()
# when
obj = RenderedMenuItem(menu_item=item)
# then
self.assertEqual(obj.menu_item, item)
self.assertIsNone(obj.count)
self.assertEqual(obj.html, "")
self.assertEqual(obj.html_id, "")
self.assertListEqual(obj.children, [])
def test_should_identify_if_item_is_a_folder(self):
# given
app_item = create_rendered_menu_item(menu_item=create_app_menu_item())
link_item = create_rendered_menu_item(menu_item=create_link_menu_item())
folder_item = create_rendered_menu_item(menu_item=create_folder_menu_item())
cases = [
(app_item, False),
(link_item, False),
(folder_item, True),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_folder, expected)
def test_should_update_html_for_link_item(self):
# given
obj = create_rendered_menu_item(menu_item=create_link_menu_item(text="Alpha"))
request = self.factory.get("/")
# when
obj.update_html(request, self.template)
# then
parsed = parse_html(obj)
self.assertEqual(parsed.text, "Alpha")
self.assertIsNone(parsed.count)
self.assertFalse(obj.html_id)
def test_should_update_html_for_folder_item(self):
# given
request = self.factory.get("/")
folder_item = create_folder_menu_item(text="Alpha")
link_item = create_link_menu_item(text="Bravo", parent=folder_item)
obj = create_rendered_menu_item(menu_item=folder_item, count=42)
rendered_link = create_rendered_menu_item(menu_item=link_item)
rendered_link.update_html(request, self.template)
obj.children.append(rendered_link)
# when
obj.update_html(request, self.template)
# then
self.assertTrue(obj.html_id)
parsed_parent = parse_html(obj)
self.assertEqual(parsed_parent.text, "Alpha")
self.assertEqual(parsed_parent.count, 42)
self.assertIn("Bravo", obj.html)
class _ParsedMenuItem(NamedTuple):
classes: List[str]
text: str
count: Optional[int]
def parse_html(obj: RenderedMenuItem) -> _ParsedMenuItem:
soup = BeautifulSoup(obj.html, "html.parser")
classes = soup.li.i.attrs["class"]
text = remove_whitespaces(soup.li.a.text)
try:
count = int(remove_whitespaces(soup.li.span.text))
except (AttributeError, ValueError):
count = None
return _ParsedMenuItem(classes=classes, text=text, count=count)

View File

@ -0,0 +1,28 @@
from django.test import TestCase
from allianceauth.menu.constants import DEFAULT_FOLDER_ICON_CLASSES
from allianceauth.menu.forms import FolderMenuItemAdminForm
class TestFolderMenuItemAdminForm(TestCase):
def test_should_set_default_icon_classes(self):
# given
form_data = {"text": "Alpha", "order": 1}
form = FolderMenuItemAdminForm(data=form_data)
# when
obj = form.save(commit=False)
# then
self.assertEqual(obj.classes, DEFAULT_FOLDER_ICON_CLASSES)
def test_should_use_icon_classes_from_input(self):
# given
form_data = {"text": "Alpha", "order": 1, "classes": "dummy"}
form = FolderMenuItemAdminForm(data=form_data)
# when
obj = form.save(commit=False)
# then
self.assertEqual(obj.classes, "dummy")

View File

@ -0,0 +1,82 @@
from django.test import RequestFactory, TestCase
from allianceauth.menu.hooks import MenuItemHook
from .factories import create_menu_item_hook
class TestMenuItemHook(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
def test_should_create_obj_with_minimal_params(self):
# when
obj = MenuItemHook("text", "classes", "url-name")
# then
self.assertEqual(obj.text, "text")
self.assertEqual(obj.classes, "classes")
self.assertEqual(obj.url_name, "url-name")
self.assertEqual(obj.template, "public/menuitem.html")
self.assertEqual(obj.order, 9999)
self.assertListEqual(obj.navactive, ["url-name"])
self.assertIsNone(obj.count)
def test_should_create_obj_with_full_params_1(self):
# when
obj = MenuItemHook("text", "classes", "url-name", 5, ["navactive"])
# then
self.assertEqual(obj.text, "text")
self.assertEqual(obj.classes, "classes")
self.assertEqual(obj.url_name, "url-name")
self.assertEqual(obj.template, "public/menuitem.html")
self.assertEqual(obj.order, 5)
self.assertListEqual(obj.navactive, ["navactive", "url-name"])
self.assertIsNone(obj.count)
def test_should_create_obj_with_full_params_2(self):
# when
obj = MenuItemHook(
text="text",
classes="classes",
url_name="url-name",
order=5,
navactive=["navactive"],
)
# then
self.assertEqual(obj.text, "text")
self.assertEqual(obj.classes, "classes")
self.assertEqual(obj.url_name, "url-name")
self.assertEqual(obj.template, "public/menuitem.html")
self.assertEqual(obj.order, 5)
self.assertListEqual(obj.navactive, ["navactive", "url-name"])
self.assertIsNone(obj.count)
def test_should_render_menu_item(self):
# given
request = self.factory.get("/")
hook = create_menu_item_hook(text="Alpha")
# when
result = hook.render(request)
# then
self.assertIn("Alpha", result)
def test_str(self):
# given
hook = create_menu_item_hook(text="Alpha")
# when/then
self.assertEqual(str(hook), "Alpha")
def test_repr(self):
# given
hook = create_menu_item_hook(text="Alpha")
# when/then
self.assertIn("Alpha", repr(hook))

View File

@ -0,0 +1,103 @@
from unittest.mock import patch
from django.db.models import QuerySet
from django.test import TestCase
from allianceauth.menu.constants import MenuItemType
from allianceauth.menu.models import MenuItem
from .factories import (
create_app_menu_item,
create_folder_menu_item,
create_link_menu_item,
create_menu_item_from_hook,
create_menu_item_hook_function,
)
from .utils import PACKAGE_PATH
class TestMenuItemQuerySet(TestCase):
def test_should_add_item_type_field(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
# when
result: QuerySet[MenuItem] = MenuItem.objects.annotate_item_type_2()
# then
for obj in [app_item, link_item, folder_item]:
obj = result.get(pk=app_item.pk)
self.assertEqual(obj.item_type_2, obj.item_type)
def test_should_filter_folders(self):
# given
create_app_menu_item()
create_link_menu_item()
folder_item = create_folder_menu_item()
# when
result: QuerySet[MenuItem] = MenuItem.objects.filter_folders()
# then
item_pks = set(result.values_list("pk", flat=True))
self.assertSetEqual(item_pks, {folder_item.pk})
@patch(PACKAGE_PATH + ".managers.get_hooks", spec=True)
class TestMenuItemManagerSyncAll(TestCase):
def test_should_create_new_items_from_hooks_when_they_do_not_exist(
self, mock_get_hooks
):
# given
mock_get_hooks.return_value = [create_menu_item_hook_function(text="Alpha")]
# when
MenuItem.objects.sync_all()
# then
self.assertEqual(MenuItem.objects.count(), 1)
obj = MenuItem.objects.first()
self.assertEqual(obj.item_type, MenuItemType.APP)
self.assertEqual(obj.text, "Alpha")
def test_should_update_existing_app_items_when_changed_only(self, mock_get_hooks):
# given
menu_hook_1 = create_menu_item_hook_function(text="Alpha", order=1)
menu_hook_2 = create_menu_item_hook_function(text="Bravo", order=2)
mock_get_hooks.return_value = [menu_hook_1, menu_hook_2]
create_menu_item_from_hook(menu_hook_1, text="name has changed", order=99)
create_menu_item_from_hook(menu_hook_2)
# when
MenuItem.objects.sync_all()
# then
self.assertEqual(MenuItem.objects.count(), 2)
obj = MenuItem.objects.get(text="Alpha")
self.assertEqual(obj.item_type, MenuItemType.APP)
self.assertEqual(obj.order, 99)
obj = MenuItem.objects.get(text="Bravo")
self.assertEqual(obj.item_type, MenuItemType.APP)
self.assertEqual(obj.order, 2)
def test_should_remove_obsolete_app_items_but_keep_user_items(self, mock_get_hooks):
# given
menu_hook = create_menu_item_hook_function(text="Alpha")
mock_get_hooks.return_value = [menu_hook]
create_app_menu_item(text="Bravo") # obsolete item
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
# when
MenuItem.objects.sync_all()
# then
self.assertEqual(MenuItem.objects.count(), 3)
obj = MenuItem.objects.get(text="Alpha")
self.assertTrue(obj.item_type, MenuItemType.APP)
self.assertIn(link_item, MenuItem.objects.all())
self.assertIn(folder_item, MenuItem.objects.all())

View File

@ -0,0 +1,166 @@
from django.test import TestCase
from allianceauth.menu.constants import MenuItemType
from .factories import (
create_app_menu_item,
create_folder_menu_item,
create_link_menu_item,
)
class TestMenuItem(TestCase):
def test_str(self):
# given
obj = create_link_menu_item()
# when
result = str(obj)
# then
self.assertIsInstance(result, str)
def test_should_return_item_type(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
cases = [
(app_item, MenuItemType.APP),
(link_item, MenuItemType.LINK),
(folder_item, MenuItemType.FOLDER),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertEqual(obj.item_type, expected)
def test_should_identify_if_item_is_a_child(self):
# given
folder = create_folder_menu_item()
child = create_link_menu_item(parent=folder)
not_child = create_link_menu_item()
cases = [(child, True), (not_child, False)]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_child, expected)
def test_should_identify_if_item_is_a_folder(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
cases = [
(app_item, False),
(link_item, False),
(folder_item, True),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_folder, expected)
def test_should_identify_if_item_is_user_defined(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
cases = [
(app_item, False),
(link_item, True),
(folder_item, True),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_user_defined, expected)
def test_should_identify_if_item_is_an_app_item(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
cases = [
(app_item, True),
(link_item, False),
(folder_item, False),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_app_item, expected)
def test_should_identify_if_item_is_a_link_item(self):
# given
app_item = create_app_menu_item()
link_item = create_link_menu_item()
folder_item = create_folder_menu_item()
cases = [
(app_item, False),
(link_item, True),
(folder_item, False),
]
# when
for obj, expected in cases:
with self.subTest(type=expected):
self.assertIs(obj.is_link_item, expected)
def test_should_not_allow_creating_invalid_app_item(self):
# when
obj = create_app_menu_item(hook_hash="")
# then
obj.refresh_from_db()
self.assertIsNone(obj.hook_hash)
class TestMenuItemToHookObj(TestCase):
def test_should_create_from_link_item(self):
# given
obj = create_link_menu_item(text="Alpha")
# when
hook_obj = obj.to_hook_obj()
# then
self.assertEqual(hook_obj.text, "Alpha")
self.assertEqual(hook_obj.url, obj.url)
self.assertEqual(hook_obj.html_id, "")
self.assertFalse(hook_obj.is_folder)
def test_should_create_from_folder(self):
# given
obj = create_folder_menu_item(text="Alpha", classes="dummy")
# when
hook_obj = obj.to_hook_obj()
# then
self.assertEqual(hook_obj.text, "Alpha")
self.assertEqual(hook_obj.classes, "dummy")
self.assertEqual(hook_obj.url, "")
self.assertTrue(hook_obj.html_id)
self.assertTrue(hook_obj.is_folder)
def test_should_create_from_folder_and_use_default_icon_classes(self):
# given
obj = create_folder_menu_item(classes="")
# when
hook_obj = obj.to_hook_obj()
# then
self.assertEqual(hook_obj.classes, "fa-solid fa-folder")
def test_should_create_from_app_item(self):
# given
obj = create_app_menu_item(text="Alpha")
# when
with self.assertRaises(ValueError):
obj.to_hook_obj()

View File

@ -0,0 +1,47 @@
import tempfile
import webbrowser
from pathlib import Path
from bs4 import BeautifulSoup
from django.http import HttpResponse
from django.template import Context, Template
PACKAGE_PATH = "allianceauth.menu"
def extract_links(response: HttpResponse) -> dict:
soup = extract_html(response)
links = {
link["href"]: "".join(link.stripped_strings)
for link in soup.find_all("a", href=True)
}
return links
def extract_html(response: HttpResponse) -> BeautifulSoup:
soup = BeautifulSoup(response.content, "html.parser")
return soup
def open_page_in_browser(response: HttpResponse):
"""Open the response in the system's default browser.
This will create a temporary file in the user's home.
"""
path = Path.home() / "temp"
path.mkdir(exist_ok=True)
with tempfile.NamedTemporaryFile(dir=path, delete=False) as file:
file.write(response.content)
webbrowser.open(file.name)
def render_template(string, context=None):
context = context or {}
context = Context(context)
return Template(string).render(context)
def remove_whitespaces(s) -> str:
return s.replace("\n", "").strip()

View File

@ -11,16 +11,20 @@
{% translate "Fleet Operation Timers" %}
{% endblock header_nav_brand %}
{% block header_nav_collapse_right %}
{% if perms.auth.optimer_management %}
<li class="nav-item">
<a class="btn btn-success" href="{% url 'optimer:add' %}">
{% translate "Create Operation" %}
</a>
</li>
{% endif %}
{% endblock header_nav_collapse_right %}
{% block content %}
<div>
<div class="text-end">
{% if perms.auth.optimer_management %}
<a href="{% url 'optimer:add' %}" class="btn btn-success">{% translate "Create Operation" %}</a>
{% endif %}
</div>
<div class="text-center mb-3">
<div class="badge bg-info text-start">
<div class="badge bg-primary text-start">
<b>{% translate "Current Eve Time:" %}</b>
<span id="current-time"></span>
</div>

View File

@ -23,7 +23,7 @@
</p>
<div class="table-responsive">
<table class="table table-striped" id="tab_permissions_audit" style="width: 100%;">
<table class="table table-striped w-100" id="tab_permissions_audit">
<thead>
<tr>
<th scope="col">{% translate "Group" %}</th>
@ -55,7 +55,7 @@
{% endblock content %}
{% block extra_javascript %}
{% include "bundles/datatables-js-bs5.html" %}
{# {% include "bundles/filterdropdown-js.html" %}#}
{% include "bundles/filterdropdown-js.html" %}
<script>
$(document).ready(() => {
@ -75,7 +75,8 @@
idx: 0,
title: 'Source'
}],
bootstrap: true
bootstrap: true,
bootstrap_version: 5
},
"stateSave": true,
"stateDuration": 0,

View File

@ -23,7 +23,7 @@
</p>
<div class="table-responsive">
<table class="table table-striped" id="tab_permissions_overview" style="width: 100%;">
<table class="table table-striped w-100" id="tab_permissions_overview">
<thead>
<tr>
<th scope="col">{% translate "App" %}</th>
@ -60,7 +60,7 @@
{% block extra_javascript %}
{% include "bundles/datatables-js-bs5.html" %}
{# {% include "bundles/filterdropdown-js.html" %}#}
{% include "bundles/filterdropdown-js.html" %}
<script>
$(document).ready(() => {
@ -86,6 +86,7 @@
}
],
bootstrap: true,
bootstrap_version: 5
},
"stateSave": true,
"stateDuration": 0,

View File

@ -8,9 +8,10 @@ If you wish to make changes, overload the setting in your project's settings fil
import os
from django.contrib import messages
from celery.schedules import crontab
from django.contrib import messages
INSTALLED_APPS = [
'allianceauth', # needs to be on top of this list to support favicons in Django admin (see https://gitlab.com/allianceauth/allianceauth/-/issues/1301)
'django.contrib.admin',
@ -73,7 +74,6 @@ PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)
MIDDLEWARE = [
'allianceauth.menu.middleware.MenuSyncMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'allianceauth.authentication.middleware.UserSettingsMiddleware',
@ -179,7 +179,7 @@ MESSAGE_TAGS = {
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1" # change the 1 here to change the database used
"LOCATION": "redis://127.0.0.1:6379/1" # change the 1 here for the DB used
}
}
@ -212,6 +212,8 @@ LOGOUT_REDIRECT_URL = 'authentication:dashboard' # destination after logging ou
# scopes required on new tokens when logging in. Cannot be blank.
LOGIN_TOKEN_SCOPES = ['publicData']
EMAIL_TIMEOUT = 15
# number of days email verification links are valid for
ACCOUNT_ACTIVATION_DAYS = 1

View File

@ -23,7 +23,7 @@
<div class="row justify-content-center">
<div class="col-md-6">
{% if generated != "" %}
<div class="text-right mb-3">
<div class="text-end mb-3">
<textarea class="form-control" rows="10" cols="60">{{ generated }}</textarea>
</div>
{% endif %}

View File

@ -1,6 +1,6 @@
{% load i18n %}
<div class="card text-center m-3" style="min-width: 18rem; min-height: 18rem;">
<div class="card text-center m-2" style="min-width: 18rem; min-height: 18rem;">
<div class="card-body">
<h5 class="card-title">{% block title %}{% endblock title %}</h5>

View File

@ -11,25 +11,33 @@
{% translate "Ship Replacement Program" %}
{% endblock header_nav_brand %}
{% block header_nav_collapse_left %}
<a class="nav-link" href="{% url 'srp:management' %}">
{% translate "View Fleets" %}
</a>
{% endblock header_nav_collapse_left %}
{% block header_nav_collapse_right %}
{% if perms.auth.srp_management %}
<li class="nav-item">
{% if fleet_status == "Completed" %}
<a class="btn btn-warning" href="{% url 'srp:mark_uncompleted' fleet_id %}">
{% translate "Mark Incomplete" %}
</a>
{% else %}
<a class="btn btn-success" href="{% url 'srp:mark_completed' fleet_id %}">
{% translate "Mark Completed" %}
</a>
{% endif %}
</li>
{% endif %}
{% endblock header_nav_collapse_right %}
{% block content %}
<div>
{% translate "SRP Fleet Data" as page_header %}
{% include "framework/header/page-header.html" with title=page_header %}
<div class="text-end mb-3">
{% if perms.auth.srp_management %}
{% if fleet_status == "Completed" %}
<a href="{% url 'srp:mark_uncompleted' fleet_id %}" class="btn btn-warning">
{% translate "Mark Incomplete" %}
</a>
{% else %}
<a href="{% url 'srp:mark_completed' fleet_id %}" class="btn btn-success">
{% translate "Mark Completed" %}
</a>
{% endif %}
{% endif %}
</div>
{% if srpfleetrequests %}
<form method="POST">
{% csrf_token %}

View File

@ -11,22 +11,26 @@
{% translate "Ship Replacement Program" %}
{% endblock header_nav_brand %}
{% block header_nav_collapse_left %}
{% if perms.auth.srp_management %}
<li class="nav-item">
<a class="nav-link" href="{% url 'srp:all' %}">
{% translate "View All" %}
</a>
</li>
{% endif %}
{% endblock header_nav_collapse_left %}
{% block header_nav_collapse_right %}
{% if perms.srp.add_srpfleetmain or perms.auth.srp_management %}
<li class="nav-item">
<a class="btn btn-success" href="{% url 'srp:add' %}">
{% translate "Add SRP Fleet" %}
</a>
</li>
{% endif %}
{% endblock header_nav_collapse_right %}
{% block content %}
<div>
<div class="text-end mb-3">
{% if perms.auth.srp_management %}
<a href="{% url 'srp:all' %}" class="btn btn-primary">
{% translate "View All" %}
</a>
{% endif %}
{% if perms.srp.add_srpfleetmain or perms.auth.srp_management %}
<a href="{% url 'srp:add' %}" class="btn btn-success">
{% translate "Add SRP Fleet" %}
</a>
{% endif %}
</div>
<div class="alert alert-info" role="alert">
<div class="text-end">
<b>{% translate "Total ISK Cost:" %} {{ totalcost | intcomma }}</b>
@ -52,9 +56,7 @@
{% for srpfleet in srpfleets %}
<tr>
<td>
<div class="badge bg-info">
{{ srpfleet.fleet_name }}
</div>
{{ srpfleet.fleet_name }}
</td>
<td>{{ srpfleet.fleet_time | date:"Y-m-d H:i" }}</td>
<td>{{ srpfleet.fleet_doctrine }}</td>
@ -93,7 +95,7 @@
<td>
<div class="badge bg-warning">{{ srpfleet.pending_requests }}</div>
</td>
<td class="text-end">
<td class="text-end text-nowrap">
<a href="{% url 'srp:fleet' srpfleet.id %}" class="btn btn-primary btn-sm m-1" title="View">
<i class="fa-solid fa-eye"></i>
</a>

View File

@ -1,239 +1,296 @@
/*
/**
* filterDropDown.js
*
* Copyright (C) 2017-18 Erik Kalkoken
* Copyright (C) 2017-24 Erik Kalkoken
*
* Extension for the jQuery plug-in DataTables (developed and tested with v1.10.15)
* Extension for the jQuery plug-in DataTables (developed and tested with v1.13.7)
*
* Version 0.4.0
*
**/
* Version 0.5.0
**/
(($) => {
"use strict";
(function ($) {
// parse initialization array and returns filterDef array to faster and easy use
// also sets defaults for properties that are not set
function parseInitArray(initArray) {
// initialization and setting defaults
let filterDef = {
"columns": [],
"columnsIdxList": [],
"bootstrap": false,
"autoSize": true,
"ajax": null,
"label": "Filter "
/**
* Parse initialization array and returns filterDef array to faster and easy use,
* also sets defaults for properties that are not set
*
* @param initArray
* @returns {{autoSize: boolean, bootstrap_version: number, columnsIdxList: *[], columns: *[], bootstrap: boolean, label: string, ajax: null}}
*/
const parseInitArray = (initArray) => {
/**
* Default filter definition
*
* @type {{autoSize: boolean, bootstrap_version: number, columnsIdxList: *[], columns: *[], bootstrap: boolean, label: string, ajax: null}}
*/
const filterDef = {
columns: [],
columnsIdxList: [],
bootstrap: false,
bootstrap_version: 3,
autoSize: true,
ajax: null,
label: "Filter ",
};
// set filter properties if they have been defined
// otherwise the defaults will be used
if (("bootstrap" in initArray) && (typeof initArray.bootstrap === 'boolean')) {
// Set filter properties if they have been defined otherwise the defaults will be used
if (
"bootstrap" in initArray &&
typeof initArray.bootstrap === "boolean"
) {
filterDef.bootstrap = initArray.bootstrap;
}
if (("autoSize" in initArray) && (typeof initArray.autoSize === 'boolean')) {
if (
"bootstrap_version" in initArray &&
typeof initArray.bootstrap_version === "number"
) {
filterDef.bootstrap_version = initArray.bootstrap_version;
}
if (
"autoSize" in initArray &&
typeof initArray.autoSize === "boolean"
) {
filterDef.autoSize = initArray.autoSize;
}
if (("ajax" in initArray) && (typeof initArray.ajax === 'string')) {
if ("ajax" in initArray && typeof initArray.ajax === "string") {
filterDef.ajax = initArray.ajax;
}
if (("label" in initArray) && (typeof initArray.label === 'string')) {
if ("label" in initArray && typeof initArray.label === "string") {
filterDef.label = initArray.label;
}
// add definition for each column
// Add definition for each column
if ("columns" in initArray) {
initArray.columns.forEach(function (initColumn) {
if (("idx" in initColumn) && (typeof initColumn.idx === 'number')) {
// initialize column
let idx = initColumn.idx;
filterDef['columns'][idx] = {
"title": null,
"maxWidth": null,
"autoSize": true
initArray.columns.forEach((initColumn) => {
if ("idx" in initColumn && typeof initColumn.idx === "number") {
// Initialize column
const idx = initColumn.idx;
filterDef.columns[idx] = {
title: null,
maxWidth: null,
autoSize: true,
};
// add to list of indices in same order they appear in the init array
filterDef['columnsIdxList'].push(idx);
// Add to a list of indices in the same order they appear in the init array
filterDef.columnsIdxList.push(idx);
// set column properties if they have been defined
// otherwise the defaults will be used
if (('title' in initColumn)
&& (typeof initColumn.title === 'string')
// Set column properties if they have been defined otherwise the defaults will be used
if (
"title" in initColumn &&
typeof initColumn.title === "string"
) {
filterDef['columns'][idx].title = initColumn.title;
filterDef.columns[idx].title = initColumn.title;
}
if (('maxWidth' in initColumn)
&& (typeof initColumn.maxWidth === 'string')
if (
"maxWidth" in initColumn &&
typeof initColumn.maxWidth === "string"
) {
filterDef['columns'][idx].maxWidth = initColumn.maxWidth;
filterDef.columns[idx].maxWidth = initColumn.maxWidth;
}
if (('autoSize' in initColumn)
&& (typeof initColumn.autoSize === 'boolean')
if (
"autoSize" in initColumn &&
typeof initColumn.autoSize === "boolean"
) {
filterDef['columns'][idx].autoSize = initColumn.autoSize;
filterDef.columns[idx].autoSize = initColumn.autoSize;
}
}
});
}
return filterDef;
}
};
// Add option d to given select object
function addOption(select, d) {
if (d != "") {
select.append('<option value="' + d + '">' + d + '</option>');
/**
* Add option d to the given select object
*
* @param select
* @param d
*/
const addOption = (select, d) => {
if (d !== "") {
select.append(`<option value="${d}">${d}</option>`);
}
}
};
// initalizing select for current column and applying event to react to changes
function initSelectForColumn(id, column) {
let select = $("#" + id + "_filterSelect" + column.index());
select.on('change', function () {
let val = $.fn.dataTable.util.escapeRegex($(this).val());
column
.search(val ? '^' + val + '$' : '', true, false)
.draw();
/**
* Initialize the select element for given column and apply event to react to changes
*
* @param id
* @param column
* @returns {*|jQuery|HTMLElement}
*/
const initSelectForColumn = (id, column) => {
const select = $(`#${id}_filterSelect${column.index()}`);
$(select).change(() => {
const val = $.fn.dataTable.util.escapeRegex($(select).val());
column.search(val ? `^${val}$` : "", true, false).draw();
});
return select
}
// Add filterDropDown container div, draw select elements with default options
// use preInit so that elements are created and correctly shown before data is loaded
$(document).on('preInit.dt', function (e, settings) {
if (e.namespace !== 'dt') return;
return select;
};
// get api object for current dt table
var api = new $.fn.dataTable.Api(settings);
// Add filterDropDown container div, draw select elements with default options.
// Use preInit so that elements are created and correctly shown before data is loaded
$(document).on("preInit.dt", (e, settings) => {
if (e.namespace !== "dt") {
return;
}
// get id of current table
var id = api.table().node().id;
// Get the api object for the current dt table
const api = new $.fn.dataTable.Api(settings);
// get initialization object for current table to retrieve custom settings
var initObj = api.init();
// Get the id of the current table
const id = api.table().node().id;
// only proceed if filter has been defined in current table, otherwise don't do anything.
if (!("filterDropDown" in initObj)) return;
// Get the initialization object for the current table to retrieve custom settings
const initObj = api.init();
// get current filter definition from init array
var filterDef = parseInitArray(initObj.filterDropDown);
// Only proceed if the filter has been defined in the current table,
// otherwise don't do anything.
if (!("filterDropDown" in initObj)) {
return;
}
// Get the current filter definition from the init array
const filterDef = parseInitArray(initObj.filterDropDown);
// only proceed if there are any columns defined
if (filterDef.columns.length == 0) return;
if (filterDef.columns.length === 0) {
return;
}
// get container div for current data table to add new elements to
var container = api.table().container();
// Get container div for the current data table to add new elements to
const container = api.table().container();
// Add filter elements to DOM
const filterWrapperId = `${id}_filterWrapper`;
// Set CSS classes for the filter wrapper div depending on bootstrap setting
let divCssClass = `${filterWrapperId} ${
filterDef.bootstrap ? "form-inline" : ""
}`;
if (filterDef.bootstrap && filterDef.bootstrap_version === 5) {
divCssClass = `${filterWrapperId} input-group my-3`;
}
// add filter elements to DOM
var filterWrapperId = id + "_filterWrapper";
var divCssClass = filterWrapperId + " " + (
(filterDef.bootstrap)
? "form-inline"
: ""
);
$(container).prepend(
'<div id="'
+ filterWrapperId
+ '" class="'
+ divCssClass + '">'
+ filterDef.label
+ '</div>'
`<div id="${filterWrapperId}" class="${divCssClass}"><span class="pt-2">${filterDef.label}</span></div>`
);
api.columns(filterDef.columnsIdxList).every(function () {
let idx = this.index();
const idx = this.index();
// set title of current column
let colName = (filterDef.columns[idx].title !== null)
? filterDef.columns[idx].title
: $(this.header()).html();
let colName =
filterDef.columns[idx].title !== null
? filterDef.columns[idx].title
: $(this.header()).html();
if (colName == "") colName = 'column ' + (idx + 1);
// adding select element for current column to container
let selectId = id + "_filterSelect" + idx;
$('#' + filterWrapperId).append(
'<select id="'
+ selectId
+ '" class="form-control '
+ id
+ '_filterSelect"></select>'
);
// initalizing select for current column and applying event to react to changes
let select = $("#" + selectId).empty()
.append('<option value="">(' + colName + ')</option>');
// set max width of select elements to current width (which is defined by the size of the title)
// turn off on for very small screens for responsive design or if autoSize has been set to false
if (filterDef.autoSize && filterDef.columns[idx].autoSize && (screen.width > 768)) {
select.css('max-width', select.outerWidth());
if (colName === "") {
colName = `column ${idx + 1}`;
}
// apply optional css style if defined in init array
// will override automatic max width setting
// Adding the select element for current column to container
const selectId = `${id}_filterSelect${idx}`;
// Set CSS classes for the select element depending on bootstrap setting
let selectMarkup = `<select id="${selectId}" class="form-control ${id}_filterSelect"></select>`;
if (filterDef.bootstrap && filterDef.bootstrap_version === 5) {
selectMarkup = `<select id="${selectId}" class="form-select w-auto ms-2 ${id}_filterSelect"></select>`;
}
$("#" + filterWrapperId).append(selectMarkup);
// Initializing select for current column and applying event to react to changes
const select = $("#" + selectId)
.empty()
.append(`<option value="">(${colName})</option>`);
// Set max width of select elements to current width (which is defined by the size of the title)
// Turn off on for very small screens for responsive design, or if autoSize has been set to false
if (
filterDef.autoSize &&
filterDef.columns[idx].autoSize &&
screen.width > 768
) {
select.css("max-width", select.outerWidth());
}
// Apply optional css style if defined in the init array will override automatic max width setting
if (filterDef.columns[idx].maxWidth !== null) {
select.css('max-width', filterDef.columns[idx].maxWidth);
select.css("max-width", filterDef.columns[idx].maxWidth);
}
});
});
// filter table and add available options to dropDowns
$(document).on('init.dt', function (e, settings) {
if (e.namespace !== 'dt') return;
// Filter table and add available options to dropDowns
$(document).on("init.dt", (e, settings) => {
if (e.namespace !== "dt") {
return;
}
// get api object for current dt table
var api = new $.fn.dataTable.Api(settings);
// Get api object for current dt table
const api = new $.fn.dataTable.Api(settings);
// get id of current table
var id = api.table().node().id;
// Get id of current table
const id = api.table().node().id;
// get initialization object for current table to retrieve custom settings
var initObj = api.init();
// Get the initialization object for current table to retrieve custom settings
const initObj = api.init();
// only proceed if filter has been defined in current table, otherwise don't do anything.
if (!("filterDropDown" in initObj)) return;
// Only proceed if a filter has been defined in the current table, otherwise don't do anything.
if (!("filterDropDown" in initObj)) {
return;
}
// get current filter definition
var filterDef = parseInitArray(initObj.filterDropDown);
// Get current filter definition
const filterDef = parseInitArray(initObj.filterDropDown);
if (filterDef.ajax == null) {
api.columns(filterDef.columnsIdxList).every(function () {
let column = this
let select = initSelectForColumn(id, column);
column.data().unique().sort().each(function (d) {
addOption(select, d)
});
const column = this;
const select = initSelectForColumn(id, column);
column
.data()
.unique()
.sort()
.each((d) => {
addOption(select, d);
});
});
} else {
// fetch column options from server for server side processing
let columnsQuery = (
"columns="
+ encodeURIComponent(
api.columns(filterDef.columnsIdxList).dataSrc().join()
)
)
$.getJSON(filterDef.ajax + "?" + columnsQuery, function (columnsOptions) {
// Fetch column options from server for server side processing
const columnsQuery = `columns=${encodeURIComponent(
api.columns(filterDef.columnsIdxList).dataSrc().join()
)}`;
$.getJSON(`${filterDef.ajax}?${columnsQuery}`, (columnsOptions) => {
api.columns(filterDef.columnsIdxList).every(function () {
let column = this;
let select = initSelectForColumn(id, column);
let columnName = column.dataSrc()
const column = this;
const select = initSelectForColumn(id, column);
const columnName = column.dataSrc();
if (columnName in columnsOptions) {
columnsOptions[columnName].forEach(function (d) {
addOption(select, d)
columnsOptions[columnName].forEach((d) => {
addOption(select, d);
});
} else {
console.warn(
"Missing column '" + columnName + "' in ajax response."
)
`Missing column '${columnName}' in ajax response.`
);
}
});
});
}
});
}(jQuery));
})(jQuery);

View File

@ -1 +1 @@
!function(t){function n(t){let n={columns:[],columnsIdxList:[],bootstrap:!1,autoSize:!0,ajax:null,label:"Filter "};return"bootstrap"in t&&"boolean"==typeof t.bootstrap&&(n.bootstrap=t.bootstrap),"autoSize"in t&&"boolean"==typeof t.autoSize&&(n.autoSize=t.autoSize),"ajax"in t&&"string"==typeof t.ajax&&(n.ajax=t.ajax),"label"in t&&"string"==typeof t.label&&(n.label=t.label),"columns"in t&&t.columns.forEach(function(t){if("idx"in t&&"number"==typeof t.idx){let e=t.idx;n.columns[e]={title:null,maxWidth:null,autoSize:!0},n.columnsIdxList.push(e),"title"in t&&"string"==typeof t.title&&(n.columns[e].title=t.title),"maxWidth"in t&&"string"==typeof t.maxWidth&&(n.columns[e].maxWidth=t.maxWidth),"autoSize"in t&&"boolean"==typeof t.autoSize&&(n.columns[e].autoSize=t.autoSize)}}),n}function e(t,n){""!=n&&t.append('<option value="'+n+'">'+n+"</option>")}function i(n,e){let i=t("#"+n+"_filterSelect"+e.index());return i.on("change",function(){let n=t.fn.dataTable.util.escapeRegex(t(this).val());e.search(n?"^"+n+"$":"",!0,!1).draw()}),i}t(document).on("preInit.dt",function(e,i){if("dt"===e.namespace){var o=new t.fn.dataTable.Api(i),l=o.table().node().id,a=o.init();if("filterDropDown"in a){var u=n(a.filterDropDown);if(0!=u.columns.length){var s=o.table().container(),c=l+"_filterWrapper",r=c+" "+(u.bootstrap?"form-inline":"");t(s).prepend('<div id="'+c+'" class="'+r+'">'+u.label+"</div>"),o.columns(u.columnsIdxList).every(function(){let n=this.index(),e=null!==u.columns[n].title?u.columns[n].title:t(this.header()).html();""==e&&(e="column "+(n+1));let i=l+"_filterSelect"+n;t("#"+c).append('<select id="'+i+'" class="form-control '+l+'_filterSelect"></select>');let o=t("#"+i).empty().append('<option value="">('+e+")</option>");u.autoSize&&u.columns[n].autoSize&&screen.width>768&&o.css("max-width",o.outerWidth()),null!==u.columns[n].maxWidth&&o.css("max-width",u.columns[n].maxWidth)})}}}}),t(document).on("init.dt",function(o,l){if("dt"===o.namespace){var a=new t.fn.dataTable.Api(l),u=a.table().node().id,s=a.init();if("filterDropDown"in s){var c=n(s.filterDropDown);if(null==c.ajax)a.columns(c.columnsIdxList).every(function(){let t=i(u,this);this.data().unique().sort().each(function(n){e(t,n)})});else{let n="columns="+encodeURIComponent(a.columns(c.columnsIdxList).dataSrc().join());t.getJSON(c.ajax+"?"+n,function(t){a.columns(c.columnsIdxList).every(function(){let n=i(u,this),o=this.dataSrc();o in t?t[o].forEach(function(t){e(n,t)}):console.warn("Missing column '"+o+"' in ajax response.")})})}}}})}(jQuery);
($=>{"use strict";const parseInitArray=initArray=>{const filterDef={columns:[],columnsIdxList:[],bootstrap:false,bootstrap_version:3,autoSize:true,ajax:null,label:"Filter "};if("bootstrap"in initArray&&typeof initArray.bootstrap==="boolean"){filterDef.bootstrap=initArray.bootstrap}if("bootstrap_version"in initArray&&typeof initArray.bootstrap_version==="number"){filterDef.bootstrap_version=initArray.bootstrap_version}if("autoSize"in initArray&&typeof initArray.autoSize==="boolean"){filterDef.autoSize=initArray.autoSize}if("ajax"in initArray&&typeof initArray.ajax==="string"){filterDef.ajax=initArray.ajax}if("label"in initArray&&typeof initArray.label==="string"){filterDef.label=initArray.label}if("columns"in initArray){initArray.columns.forEach(initColumn=>{if("idx"in initColumn&&typeof initColumn.idx==="number"){const idx=initColumn.idx;filterDef.columns[idx]={title:null,maxWidth:null,autoSize:true};filterDef.columnsIdxList.push(idx);if("title"in initColumn&&typeof initColumn.title==="string"){filterDef.columns[idx].title=initColumn.title}if("maxWidth"in initColumn&&typeof initColumn.maxWidth==="string"){filterDef.columns[idx].maxWidth=initColumn.maxWidth}if("autoSize"in initColumn&&typeof initColumn.autoSize==="boolean"){filterDef.columns[idx].autoSize=initColumn.autoSize}}})}return filterDef};const addOption=(select,d)=>{if(d!==""){select.append(`<option value="${d}">${d}</option>`)}};const initSelectForColumn=(id,column)=>{const select=$(`#${id}_filterSelect${column.index()}`);$(select).change(()=>{const val=$.fn.dataTable.util.escapeRegex($(select).val());column.search(val?`^${val}$`:"",true,false).draw()});return select};$(document).on("preInit.dt",(e,settings)=>{if(e.namespace!=="dt"){return}const api=new $.fn.dataTable.Api(settings);const id=api.table().node().id;const initObj=api.init();if(!("filterDropDown"in initObj)){return}const filterDef=parseInitArray(initObj.filterDropDown);if(filterDef.columns.length===0){return}const container=api.table().container();const filterWrapperId=`${id}_filterWrapper`;let divCssClass=`${filterWrapperId} ${filterDef.bootstrap?"form-inline":""}`;if(filterDef.bootstrap&&filterDef.bootstrap_version===5){divCssClass=`${filterWrapperId} input-group my-3`}$(container).prepend(`<div id="${filterWrapperId}" class="${divCssClass}"><span class="pt-2">${filterDef.label}</span></div>`);api.columns(filterDef.columnsIdxList).every(function(){const idx=this.index();let colName=filterDef.columns[idx].title!==null?filterDef.columns[idx].title:$(this.header()).html();if(colName===""){colName=`column ${idx+1}`}const selectId=`${id}_filterSelect${idx}`;let selectMarkup=`<select id="${selectId}" class="form-control ${id}_filterSelect"></select>`;if(filterDef.bootstrap&&filterDef.bootstrap_version===5){selectMarkup=`<select id="${selectId}" class="form-select w-auto ms-2 ${id}_filterSelect"></select>`}$("#"+filterWrapperId).append(selectMarkup);const select=$("#"+selectId).empty().append(`<option value="">(${colName})</option>`);if(filterDef.autoSize&&filterDef.columns[idx].autoSize&&screen.width>768){select.css("max-width",select.outerWidth())}if(filterDef.columns[idx].maxWidth!==null){select.css("max-width",filterDef.columns[idx].maxWidth)}})});$(document).on("init.dt",(e,settings)=>{if(e.namespace!=="dt"){return}const api=new $.fn.dataTable.Api(settings);const id=api.table().node().id;const initObj=api.init();if(!("filterDropDown"in initObj)){return}const filterDef=parseInitArray(initObj.filterDropDown);if(filterDef.ajax==null){api.columns(filterDef.columnsIdxList).every(function(){const column=this;const select=initSelectForColumn(id,column);column.data().unique().sort().each(d=>{addOption(select,d)})})}else{const columnsQuery=`columns=${encodeURIComponent(api.columns(filterDef.columnsIdxList).dataSrc().join())}`;$.getJSON(`${filterDef.ajax}?${columnsQuery}`,columnsOptions=>{api.columns(filterDef.columnsIdxList).every(function(){const column=this;const select=initSelectForColumn(id,column);const columnName=column.dataSrc();if(columnName in columnsOptions){columnsOptions[columnName].forEach(d=>{addOption(select,d)})}else{console.warn(`Missing column '${columnName}' in ajax response.`)}})})}})})(jQuery);

View File

@ -0,0 +1,37 @@
{% load i18n %}
<div id="esi-alert" class="col-12 align-self-stretch py-2 collapse">
<div class="alert alert-warning">
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
<hr>
<pre id="esi-data" class="text-center text-wrap"></pre>
</div>
</div>
<script>
const elemCard = document.getElementById("esi-alert");
const elemMessage = document.getElementById("esi-data");
const elemCode = document.getElementById("esi-code");
fetch('{% url "authentication:esi_check" %}')
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Something went wrong");
})
.then((responseJson) => {
console.log("ESI Check: ", JSON.stringify(responseJson, null, 2));
const status = responseJson.status;
if (status != 200) {
elemCode.textContent = status
elemMessage.textContent = responseJson.data.error;
new bootstrap.Collapse(elemCard, {
toggle: true
})
}
})
.catch((error) => {
console.log(error);
});
</script>

View File

@ -2,7 +2,7 @@
{% load humanize %}
{% if notifications %}
<div class="col-12 align-self-stretch pb-2">
<div id="aa-dashboard-panel-admin-notifications" class="col-12 align-self-stretch pb-2">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
@ -51,10 +51,10 @@
</div>
{% endif %}
<div class="col-12 align-self-stretch py-2">
<div class="col-12 align-self-stretch pb-2">
<div class="card">
<div class="card-body d-flex flex-row flex-wrap">
<div class="col-xl-6 col-lg-12 col-md-12 col-sm-12">
<div id="aa-dashboard-panel-software-version" class="col-xl-6 col-lg-12 col-md-12 col-sm-12">
<h4 class="ms-auto me-auto text-center">
{% translate "Software Version" %}
</h4>
@ -97,7 +97,7 @@
</div>
</div>
<div class="col-xl-6 col-lg-12 col-md-12 col-sm-12">
<div id="aa-dashboard-panel-task-queue" class="col-xl-6 col-lg-12 col-md-12 col-sm-12">
<h4 class="ms-auto me-auto text-center">
{% translate "Task Queue" %}
</h4>

View File

@ -34,12 +34,6 @@
padding-top: {% header_padding_size %} !important;
}
{% endif %}
.auth-logo {
background-position: bottom;
background-repeat: no-repeat;
background-image: url("{% static 'allianceauth/images/auth-logo.png' %}") ;
}
</style>
{% block extra_css %}{% endblock extra_css %}
</head>
@ -59,20 +53,18 @@
{% block header_nav_brand %}{{ SITE_NAME }}{% endblock %}
</div>
<div class="collapse navbar-collapse" id="navbarexpand">
<div class="m-2"></div>
<ul id="nav-left" class="navbar-nav nav me-auto">
<div class="collapse navbar-collapse ms-2 px-2" id="navbarexpand">
<ul id="nav-left" class="nav navbar-nav me-auto">
{% block header_nav_collapse_left %}
{% endblock %}
</ul>
<ul id="nav-right" class="navbar-nav">
<ul id="nav-right" class="nav navbar-nav">
{% block header_nav_collapse_right %}
{% endblock %}
</ul>
<ul id="nav-right-character-control" class="navbar-nav">
<ul id="nav-right-character-control" class="nav navbar-nav">
{% block header_nav_user_character_control %} <!-- Default to add char and swap main -->
{% include 'allianceauth/top-menu-rh-default.html' %}
{% endblock %}

View File

@ -1,6 +1,6 @@
{% load i18n %}
{% load navactive %}
{% load menu_menu_items %}
{% load menu_items %}
<div class="col-sm-2 auth-side-navbar" role="navigation">
<div class="collapse navbar-collapse auth-menus-collapse auth-side-navbar-collapse">

View File

@ -1,6 +1,6 @@
{% load i18n %}
<li class="nav-item active">
<li class="nav-item">
<a href="{% url 'authentication:add_character' %}" class="nav-link" title="{% translate 'Add Character' %}">
<i class="fa-solid fa-plus"></i>
<span class="d-lg-none d-md-inline m-2">{% translate "Add Character" %}</span>

View File

@ -0,0 +1,3 @@
{% load static %}
<img src="{% static 'allianceauth/images/auth-logo.svg' %}" width="{{ logo_width|default:"128px" }}" height="{% if logo_height %}{{ logo_ }}{% else %}{{ logo_width|default:"128px" }}{% endif %}" alt="{{ SITE_NAME }}">

View File

@ -2,6 +2,20 @@ from allianceauth import hooks
from allianceauth.theme.hooks import ThemeHook
CSS_STATICS = [{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css",
"integrity": "sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
}]
JS_STATICS = [{
"url": "https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js",
"integrity": "sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
}, {
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js",
"integrity": "sha512-WW8/jxkELe2CAiE4LvQfwm1rajOS8PHasCCx+knHG0gBHt8EXxS6T6tJRTGuDQVnluuAvMxWF4j8SNFDKceLFg=="
}]
class BootstrapThemeHook(ThemeHook):
"""
Bootstrap in all its glory!
@ -13,21 +27,35 @@ class BootstrapThemeHook(ThemeHook):
self,
"Bootstrap",
"Powerful, extensible, and feature-packed frontend toolkit.",
css=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css",
"integrity": "sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
}],
js=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js",
"integrity": "sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
}, {
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js",
"integrity": "sha512-WW8/jxkELe2CAiE4LvQfwm1rajOS8PHasCCx+knHG0gBHt8EXxS6T6tJRTGuDQVnluuAvMxWF4j8SNFDKceLFg=="
}],
css=CSS_STATICS,
js=JS_STATICS,
header_padding="3.5em"
)
class BootstrapDarkThemeHook(ThemeHook):
"""
Bootstrap in all its glory!, but _dark_
https://getbootstrap.com/
"""
def __init__(self):
ThemeHook.__init__(
self,
"Bootstrap Dark",
"Powerful, extensible, and feature-packed frontend toolkit.",
css=CSS_STATICS,
js=JS_STATICS,
html_tags="data-bs-theme=dark",
header_padding="3.5em"
)
@hooks.register('theme_hook')
def register_bootstrap_dark_hook():
return BootstrapDarkThemeHook()
@hooks.register('theme_hook')
def register_bootstrap_hook():
return BootstrapThemeHook()

View File

@ -1,11 +0,0 @@
from django.apps import AppConfig
class BootstrapDarkThemeConfig(AppConfig):
name = "allianceauth.theme.bootstrap_dark"
label = "bootstrap_dark"
version = "5.3.0"
verbose_name = f"Bootstrap Dark v{version}"
def ready(self):
pass

View File

@ -1,34 +0,0 @@
from allianceauth import hooks
from allianceauth.theme.hooks import ThemeHook
class BootstrapDarkThemeHook(ThemeHook):
"""
Bootstrap in all its glory!, but _dark_
https://getbootstrap.com/
"""
def __init__(self):
ThemeHook.__init__(
self,
"Bootstrap Dark",
"Powerful, extensible, and feature-packed frontend toolkit.",
css=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css",
"integrity": "sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
}],
js=[{
"url": "https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js",
"integrity": "sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
}, {
"url": "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js",
"integrity": "sha512-WW8/jxkELe2CAiE4LvQfwm1rajOS8PHasCCx+knHG0gBHt8EXxS6T6tJRTGuDQVnluuAvMxWF4j8SNFDKceLFg=="
}],
html_tags="data-bs-theme=dark",
header_padding="3.5em"
)
@hooks.register('theme_hook')
def register_bootstrap_dark_hook():
return BootstrapDarkThemeHook()

Some files were not shown because too many files have changed in this diff Show More