Compare commits

..

34 Commits

Author SHA1 Message Date
Ariel Rin
9cfebc9ae3 Version Bump to 2.7.4 2020-08-17 06:34:55 +00:00
Ariel Rin
ddabb4539b Merge branch 'fix_admin_status_tags_bug' into 'master'
Bugfix: Loading of dashboard fails with 'NoneType object is not iterable' from status_tags

See merge request allianceauth/allianceauth!1237
2020-08-14 02:26:12 +00:00
Ariel Rin
ada35e221b Merge branch 'transifex' into 'master'
Update from Transifex

See merge request allianceauth/allianceauth!1241
2020-08-14 02:24:07 +00:00
Ariel Rin
6fef9d904e Update from Transifex 2020-08-14 02:24:07 +00:00
Ariel Rin
67cf2b5904 Merge branch 'fontawesomev5' into 'master'
Remove FA v4 shims, bump FA to 5.14

Closes #1248

See merge request allianceauth/allianceauth!1242
2020-08-14 02:21:41 +00:00
Ariel Rin
3bebe792f6 Remove FA v4 shims, bump to fa 5.14 2020-08-14 12:01:08 +10:00
ErikKalkoken
00b4d89181 Fix status tags bug and remove unused context from status_overview tag 2020-07-27 14:53:25 +02:00
Ariel Rin
f729c6b650 Merge branch 'patch-3' into 'master'
Update Mumble documentation on setting a server password

Closes #1252

See merge request allianceauth/allianceauth!1236
2020-07-24 11:18:02 +00:00
colcrunch
df95f8c3f3 Merge branch 'discord_improvements' into 'master'
Fix error 500 on service page for Discord and add feature "group_to_role"

See merge request allianceauth/allianceauth!1235
2020-07-23 20:58:26 +00:00
Erik Kalkoken
fe36e57d72 Fix error 500 on service page for Discord and add feature "group_to_role" 2020-07-23 20:58:26 +00:00
Peter Pfeufer
31197812b6 Update Mumble documentation on setting a server password (#1252) 2020-07-22 12:18:52 +00:00
Ariel Rin
bd3fe01a12 correct task import for manual model population 2020-07-14 12:25:05 +00:00
col_crunch
39f7f32b7d Version bump. 2020-07-13 15:32:54 -04:00
colcrunch
b4522a1277 Merge branch 'enable_django_esi_20' into 'master'
Enable django-esi 2.0 dependency

See merge request allianceauth/allianceauth!1232
2020-07-13 18:54:21 +00:00
colcrunch
bb6a7e8327 Merge branch 'issue_1250' into 'master'
Fix Discord service issues and improve dashboard

Closes #1250

See merge request allianceauth/allianceauth!1229
2020-07-13 18:54:08 +00:00
colcrunch
9bd42a7579 Merge branch 'docu_update_contributions' into 'master'
Add contributing chapter to docs

See merge request allianceauth/allianceauth!1233
2020-07-13 18:52:30 +00:00
Erik Kalkoken
b41430e5a3 Add contributing chapter to docs 2020-07-13 18:52:29 +00:00
ErikKalkoken
595353e838 Enable using django-esi 2.0 2020-07-11 12:35:16 +02:00
ErikKalkoken
f1a21bb856 Add state info to dashboard, improve stat change notifications, improve auth tests 2020-07-03 14:58:45 +02:00
ErikKalkoken
e44c2935f9 Version bump for alpha testing 2020-07-03 13:29:00 +02:00
ErikKalkoken
4d546f948d Fix state role not always updated due to lazy properties 2020-07-02 21:26:40 +02:00
ErikKalkoken
3bab349d7b Fix tests 2020-06-30 00:15:48 +02:00
ErikKalkoken
eef6126ef8 Improve state backend fix, add tests for state backend and service signal changes 2020-06-30 00:01:40 +02:00
ErikKalkoken
5c7478fa39 Fix update_nickname runs on ever save of userprofile, fix nickname not updated after main's name changes 2020-06-28 21:38:25 +02:00
ErikKalkoken
64b72d0b06 Fix service signals for state change 2020-06-28 17:14:16 +02:00
ErikKalkoken
b266a98b25 Add more tests for state change 2020-06-28 14:56:01 +02:00
ErikKalkoken
8a27de5df8 Fix state change does not update groups 2020-06-28 01:45:51 +02:00
ErikKalkoken
f9b5310fce Fix user account not deleted when demoted to guest 2020-06-27 23:32:12 +02:00
ErikKalkoken
fdce173969 Add tests to fix new bugs 2020-06-27 16:18:06 +02:00
ErikKalkoken
7b9ddf90c1 Fix tests for tox 2020-06-25 23:38:20 +02:00
ErikKalkoken
580c8c19de Update request timeout default 2020-06-25 22:32:29 +02:00
ErikKalkoken
55cc77140e Fix bug blocking superuser from adding Discord bot 2020-06-25 22:19:48 +02:00
Ariel Rin
93c89dd7cc Merge branch 'wother-master-patch-74872' into 'master'
Updated discord.md

See merge request allianceauth/allianceauth!1228
2020-06-22 03:19:27 +00:00
Carter Foulger
c970cbbd2d Updated discord.md with additional troubleshooting
steps.
2020-06-19 19:45:22 +00:00
43 changed files with 2034 additions and 812 deletions

View File

@@ -1,7 +1,7 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.7.2'
__version__ = '2.7.4'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__)

View File

@@ -1,7 +1,8 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
import logging
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, Permission
from .models import UserProfile, CharacterOwnership, OwnershipRecord
@@ -11,9 +12,11 @@ logger = logging.getLogger(__name__)
class StateBackend(ModelBackend):
@staticmethod
def _get_state_permissions(user_obj):
profile_state_field = UserProfile._meta.get_field('state')
user_state_query = 'state__%s__user' % profile_state_field.related_query_name()
return Permission.objects.filter(**{user_state_query: user_obj})
"""returns permissions for state of given user object"""
if hasattr(user_obj, "profile") and user_obj.profile:
return Permission.objects.filter(state=user_obj.profile.state)
else:
return Permission.objects.none()
def get_state_permissions(self, user_obj, obj=None):
return self._get_permissions(user_obj, obj, 'state')

View File

@@ -73,11 +73,17 @@ class UserProfile(models.Model):
if commit:
logger.info('Updating {} state to {}'.format(self.user, self.state))
self.save(update_fields=['state'])
notify(self.user, _('State Changed'),
_('Your user state has been changed to %(state)s') % ({'state': state}),
'info')
notify(
self.user,
_('State changed to: %s' % state),
_('Your user\'s state is now: %(state)s')
% ({'state': state}),
'info'
)
from allianceauth.authentication.signals import state_changed
state_changed.send(sender=self.__class__, user=self.user, state=self.state)
state_changed.send(
sender=self.__class__, user=self.user, state=self.state
)
def __str__(self):
return str(self.user)

View File

@@ -23,9 +23,7 @@ def trigger_state_check(state):
check_states = State.objects.filter(priority__lt=state.priority)
for profile in UserProfile.objects.filter(state__in=check_states):
if state.available_to_user(profile.user):
profile.state = state
profile.save(update_fields=['state'])
state_changed.send(sender=state.__class__, user=profile.user, state=state)
profile.assign_state(state)
@receiver(m2m_changed, sender=State.member_characters.through)

View File

@@ -14,7 +14,11 @@
<div class="col-sm-6 text-center">
<div class="panel panel-primary" style="height:100%">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Main Character" %}</h3>
<h3 class="panel-title">
{% blocktrans with state=request.user.profile.state %}
Main Character (State: {{ state }})
{% endblocktrans %}
</h3>
</div>
<div class="panel-body">
{% if request.user.profile.main_character %}

View File

@@ -0,0 +1,149 @@
from django.contrib.auth.models import User, Group
from django.test import TestCase
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
from esi.models import Token
from ..backends import StateBackend
from ..models import CharacterOwnership, UserProfile, OwnershipRecord
MODULE_PATH = 'allianceauth.authentication'
PERMISSION_1 = "authentication.add_user"
PERMISSION_2 = "authentication.change_user"
class TestStatePermissions(TestCase):
def setUp(self):
# permissions
self.permission_1 = AuthUtils.get_permission_by_name(PERMISSION_1)
self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2)
# group
self.group_1 = Group.objects.create(name="Group 1")
self.group_2 = Group.objects.create(name="Group 2")
# state
self.state_1 = AuthUtils.get_member_state()
self.state_2 = AuthUtils.create_state("Other State", 75)
# user
self.user = AuthUtils.create_user("Bruce Wayne")
self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123)
def test_user_has_user_permissions(self):
self.user.user_permissions.add(self.permission_1)
user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1))
def test_user_has_group_permissions(self):
self.group_1.permissions.add(self.permission_1)
self.user.groups.add(self.group_1)
user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1))
def test_user_has_state_permissions(self):
self.state_1.permissions.add(self.permission_1)
self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1))
def test_when_user_changes_state_perms_change_accordingly(self):
self.state_1.permissions.add(self.permission_1)
self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1))
self.state_2.permissions.add(self.permission_2)
self.state_2.member_characters.add(self.main)
self.state_1.member_characters.remove(self.main)
user = User.objects.get(pk=self.user.pk)
self.assertFalse(user.has_perm(PERMISSION_1))
self.assertTrue(user.has_perm(PERMISSION_2))
def test_state_permissions_are_returned_for_current_user_object(self):
# verify state permissions are returns for the current user object
# and not for it's instance in the database, which might be outdated
self.state_1.permissions.add(self.permission_1)
self.state_2.permissions.add(self.permission_2)
self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk)
user.profile.state = self.state_2
self.assertFalse(user.has_perm(PERMISSION_1))
self.assertTrue(user.has_perm(PERMISSION_2))
class TestAuthenticate(TestCase):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))

View File

@@ -0,0 +1,35 @@
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..models import CharacterOwnership, UserProfile
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@@ -0,0 +1,68 @@
from unittest import mock
from urllib import parse
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponse
from django.shortcuts import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
from ..decorators import main_character_required
from ..models import CharacterOwnership
MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user(
'no_main_user', disconnect_signals=True
)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(
user=cls.main_user, character=main_character, owner_hash='1'
)
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)

View File

@@ -1,147 +1,20 @@
from unittest import mock
from io import StringIO
from urllib import parse
from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User
from django.core.management import call_command
from django.http.response import HttpResponse
from django.shortcuts import reverse
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.client import RequestFactory
from allianceauth.authentication.decorators import main_character_required
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils
from esi.errors import IncompleteResponseError
from esi.models import Token
from ..backends import StateBackend
from ..models import CharacterOwnership, UserProfile, State, get_guest_state,\
OwnershipRecord
from ..models import CharacterOwnership, State, get_guest_state
from ..tasks import check_character_ownership
MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)
class BackendTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))
class CharacterOwnershipTestCase(TestCase):
@classmethod
def setUpTestData(cls):
@@ -378,30 +251,3 @@ class CharacterOwnershipCheckTestCase(TestCase):
filter.return_value.exists.return_value = False
check_character_ownership(self.ownership)
self.assertTrue(filter.return_value.delete.called)
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@@ -83,9 +83,8 @@ class TestStatusOverviewTag(TestCase):
}
mock_current_version_info.return_value = version_info
mock_fetch_celery_queue_length.return_value = 3
context = {}
result = status_overview(context)
result = status_overview()
expected = {
'notifications': GITHUB_NOTIFICATION_ISSUES[:5],
'latest_major': True,
@@ -128,6 +127,13 @@ class TestNotifications(TestCase):
result = _current_notifications()
self.assertEqual(result['notifications'], list())
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_is_none(self, mock_cache):
mock_cache.get_or_set.return_value = None
result = _current_notifications()
self.assertEqual(result['notifications'], list())
class TestCeleryQueueLength(TestCase):
@@ -170,6 +176,15 @@ class TestVersionTags(TestCase):
result = _fetch_tags_from_gitlab()
self.assertEqual(result, GITHUB_TAGS)
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_return_no_data(self, mock_cache):
mock_cache.get_or_set.return_value = None
expected = {}
result = _current_version_summary()
self.assertEqual(result, expected)
class TestLatestsVersion(TestCase):

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-08 00:57+0000\n"
"POT-Creation-Date: 2020-07-29 04:56+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Rounon Dax <rounon.dax@terra-nanotech.de>, 2020\n"
"Language-Team: German (https://www.transifex.com/alliance-auth/teams/107430/de/)\n"
@@ -32,55 +32,55 @@ msgstr ""
msgid "Email"
msgstr "E-Mail"
#: allianceauth/authentication/models.py:76
msgid "State Changed"
msgstr "Status geändert"
#: allianceauth/authentication/models.py:77
#: allianceauth/authentication/models.py:78
#, python-format
msgid "Your user state has been changed to %(state)s"
msgstr "Dein Nutzerstatus hat sich geändert zu %(state)s"
msgid "State changed to: %s"
msgstr "Status geändert zu %s"
#: allianceauth/authentication/models.py:79
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Dein Nutzerstatus ist nun %(state)s"
#: allianceauth/authentication/templates/authentication/dashboard.html:5
#: allianceauth/authentication/templates/authentication/dashboard.html:8
#: allianceauth/templates/allianceauth/side-menu.html:10
#: allianceauth/templates/allianceauth/side-menu.html:12
msgid "Dashboard"
msgstr "Dashboard"
#: allianceauth/authentication/templates/authentication/dashboard.html:17
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "Hauptcharakter"
#: allianceauth/authentication/templates/authentication/dashboard.html:18
#, python-format
msgid ""
"\n"
" Main Character (State: %(state)s)\n"
" "
msgstr ""
"\n"
"Hauptcharakter (Status: %(state)s)"
#: allianceauth/authentication/templates/authentication/dashboard.html:77
#: allianceauth/authentication/templates/authentication/dashboard.html:81
msgid "No main character set."
msgstr "Kein Hauptcharakter gesetzt."
#: allianceauth/authentication/templates/authentication/dashboard.html:84
#: allianceauth/authentication/templates/authentication/dashboard.html:88
msgid "Add Character"
msgstr "Charakter hinzufügen"
#: allianceauth/authentication/templates/authentication/dashboard.html:88
#: allianceauth/authentication/templates/authentication/dashboard.html:92
msgid "Change Main"
msgstr "Hauptcharakter ändern"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/authentication/templates/authentication/dashboard.html:101
msgid "Group Memberships"
msgstr "Gruppen"
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/authentication/templates/authentication/dashboard.html:121
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters"
msgstr "Charaktere"
#: allianceauth/authentication/templates/authentication/dashboard.html:125
#: allianceauth/authentication/templates/authentication/dashboard.html:129
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:22
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -89,13 +89,13 @@ msgstr "Charaktere"
msgid "Name"
msgstr "Name"
#: allianceauth/authentication/templates/authentication/dashboard.html:126
#: allianceauth/authentication/templates/authentication/dashboard.html:130
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp"
msgstr "Corp"
#: allianceauth/authentication/templates/authentication/dashboard.html:127
#: allianceauth/authentication/templates/authentication/dashboard.html:131
#: allianceauth/corputils/templates/corputils/corpstats.html:77
#: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance"
@@ -236,8 +236,8 @@ msgstr "Letzes Update:"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:28
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:27
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:37
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:96
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:51
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:110
msgid "Character"
msgstr "Charakter"
@@ -259,6 +259,16 @@ msgstr "Corporation"
msgid "Killboard"
msgstr "Killboard"
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "Hauptcharakter"
#: allianceauth/corputils/templates/corputils/corpstats.html:117
#: allianceauth/corputils/templates/corputils/search.html:17
msgid "Main Corporation"
@@ -594,8 +604,8 @@ msgid "Portrait"
msgstr "Portrait"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:30
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:38
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:52
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:111
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:23
msgid "Organization"
msgstr "Organization"
@@ -614,7 +624,7 @@ msgstr "Gruppenmitgliedschaft"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:17
msgid "Groups"
msgstr "Gruppen"
@@ -658,8 +668,8 @@ msgid "Audit Members"
msgstr "Mitglieder Protokoll"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:56
msgid "Copy Direrct Join Link"
msgstr ""
msgid "Copy Direct Join Link"
msgstr "Direktlink kopieren"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:68
msgid "No groups to list."
@@ -690,37 +700,37 @@ msgstr "Keine Gruppen verfügbar"
msgid "Groups Management"
msgstr "Gruppenverwaltung"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:25
msgid "Join Requests"
msgstr "Beitrittsgesuche"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:33
msgid "Leave Requests"
msgstr "Austrittsgesuche"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:39
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:98
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:53
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:20
#: allianceauth/services/modules/openfire/forms.py:6
msgid "Group"
msgstr "Gruppen"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:71
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:130
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:85
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:144
msgid "Accept"
msgstr "Akzeptieren"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:74
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:133
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:88
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:147
#: allianceauth/hrapplications/templates/hrapplications/view.html:85
msgid "Reject"
msgstr "Ablehnen"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:83
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
msgid "No group add requests."
msgstr "Keine Gruppenbeitrittsanfragen."
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:142
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:156
msgid "No group leave requests."
msgstr "Keine Gruppenaustrittsanfragen"
@@ -729,7 +739,7 @@ msgid "Toggle navigation"
msgstr "Toggle Navigation"
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:23
#: allianceauth/templates/allianceauth/side-menu.html:25
msgid "Group Management"
msgstr "Gruppenverwaltung"
@@ -741,26 +751,26 @@ msgstr "Gruppenanfragen"
msgid "Group Membership"
msgstr "Gruppenmitgliedschaft"
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:162
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "Benutzer %(user)s von Gruppe %(group)s entfernt."
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:164
msgid "User does not exist in that group"
msgstr "Benutzer existiert nicht in dieser Gruppe"
#: allianceauth/groupmanagement/views.py:171
#: allianceauth/groupmanagement/views.py:167
msgid "Group does not exist"
msgstr "Gruppe existiert nicht"
#: allianceauth/groupmanagement/views.py:198
#: allianceauth/groupmanagement/views.py:194
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s zugestimmt."
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#: allianceauth/groupmanagement/views.py:201
#: allianceauth/groupmanagement/views.py:234
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
@@ -769,18 +779,18 @@ msgstr ""
"Bei der Bearbeitung des Beitrittsgesuchs von %(mainchar)s zur Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:231
#: allianceauth/groupmanagement/views.py:227
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "Beitrittsgesuch von %(mainchar)s zur Gruppe %(group)s abgelehnt."
#: allianceauth/groupmanagement/views.py:267
#: allianceauth/groupmanagement/views.py:263
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s akzeptiert."
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#: allianceauth/groupmanagement/views.py:269
#: allianceauth/groupmanagement/views.py:303
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
@@ -789,26 +799,26 @@ msgstr ""
"Bei der Bearbeitung des Austrittsgesuchs von %(mainchar)s für Gruppe "
"%(group)s ist ein unbehandelter Fehler aufgetreten."
#: allianceauth/groupmanagement/views.py:300
#: allianceauth/groupmanagement/views.py:296
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Austrittsgesuch von %(mainchar)s für Gruppe %(group)s abgelehnt."
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:342
#: allianceauth/groupmanagement/views.py:354
msgid "You cannot join that group"
msgstr "Du kannst dieser Gruppe nicht beitreten"
#: allianceauth/groupmanagement/views.py:352
#: allianceauth/groupmanagement/views.py:348
msgid "You are already a member of that group."
msgstr "Du bist bereits Mitglied dieser Gruppe."
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:363
msgid "You already have a pending application for that group."
msgstr "Du hast Dich bereits für diese Gruppe beworben."
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/groupmanagement/views.py:366
#: allianceauth/groupmanagement/views.py:404
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -820,24 +830,24 @@ msgstr "Du hast Dich bereits für diese Gruppe beworben."
msgid "Pending"
msgstr "Beantragt"
#: allianceauth/groupmanagement/views.py:376
#: allianceauth/groupmanagement/views.py:372
#, python-format
msgid "Applied to group %(group)s."
msgstr "Beitritt zur Gruppe %(group)s beantragt."
#: allianceauth/groupmanagement/views.py:387
#: allianceauth/groupmanagement/views.py:383
msgid "You cannot leave that group"
msgstr "Du kannst diese Gruppe nicht verlassen"
#: allianceauth/groupmanagement/views.py:392
#: allianceauth/groupmanagement/views.py:388
msgid "You are not a member of that group"
msgstr "Du bist kein Mitglied dieser Gruppe"
#: allianceauth/groupmanagement/views.py:401
#: allianceauth/groupmanagement/views.py:397
msgid "You already have a pending leave request for that group."
msgstr "Du hast bereits ein ausstehendes Austrittsgesuch für diese Gruppe."
#: allianceauth/groupmanagement/views.py:414
#: allianceauth/groupmanagement/views.py:410
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Austrittsgesuch für Gruppe %(group)s gesendet."
@@ -1300,23 +1310,54 @@ msgstr "Passwort"
msgid "Password must be at least 8 characters long."
msgstr "Passwort muss mindestens 8 Zeichen lang sein"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:23
#: allianceauth/services/modules/discord/models.py:224
msgid "Discord Account Disabled"
msgstr "Discord Konto deaktiviert"
#: allianceauth/services/modules/discord/models.py:226
msgid ""
"Your Discord account was disabeled automatically by Auth. If you think this "
"was a mistake, please contact an admin."
msgstr ""
"Dein Discord Konto wurde automatisch durch Auth deaktiviert. Wenn Du glaubst"
" dies war ein Fehler, kontaktiere bitte einen Administrator."
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:18
msgid "Join the Discord server"
msgstr "Discord Server beitreten"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:22
msgid "Leave- and rejoin the Discord Server (Reset)"
msgstr "Discord Server verlassen und wieder beitreten (Zurücksetzen)"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:25
msgid "Leave the Discord server"
msgstr "Discord Server verlassen"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32
msgid "Link Discord Server"
msgstr "Verbinde Discord Server"
#: allianceauth/services/modules/discord/views.py:26
#: allianceauth/services/modules/discord/views.py:30
msgid "Deactivated Discord account."
msgstr "Discord Konto deaktiviert."
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
#: allianceauth/services/modules/discord/views.py:36
#: allianceauth/services/modules/discord/views.py:59
msgid "An error occurred while processing your Discord account."
msgstr "Es gab einen Fehler bei der Verarbeitung Deines Discord Kontos."
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
msgstr "Discord Konto aktiviert."
#: allianceauth/services/modules/discord/views.py:102
msgid "Your Discord account has been successfully activated."
msgstr "Dein Discord Konto wurde erfolgreich aktiviert."
#: allianceauth/services/modules/discord/views.py:108
msgid ""
"An error occurred while trying to activate your Discord account. Please try "
"again."
msgstr ""
"Es gab einen Fehler während der Aktivierung Deines Discord Kontos. Bitte "
"versuche es noch einmal."
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
@@ -1884,32 +1925,30 @@ msgid "Current"
msgstr "Aktuell"
#: allianceauth/templates/allianceauth/admin-status/overview.html:40
msgid "Latest Major"
msgstr "Aktuellste Hauptversion"
msgid "Latest Stable"
msgstr "Aktuellste stabile Version"
#: allianceauth/templates/allianceauth/admin-status/overview.html:46
#: allianceauth/templates/allianceauth/admin-status/overview.html:56
#: allianceauth/templates/allianceauth/admin-status/overview.html:66
msgid "Update available"
msgstr "Update verfügbar"
#: allianceauth/templates/allianceauth/admin-status/overview.html:50
msgid "Latest Minor"
msgstr "Aktuellste Unterversion"
#: allianceauth/templates/allianceauth/admin-status/overview.html:51
msgid "Latest Pre-Release"
msgstr "Aktuellste Testversion"
#: allianceauth/templates/allianceauth/admin-status/overview.html:60
msgid "Latest Patch"
msgstr "Aktuellste Patchversion"
#: allianceauth/templates/allianceauth/admin-status/overview.html:57
msgid "Pre-Release available"
msgstr "Testversion verfügbar"
#: allianceauth/templates/allianceauth/admin-status/overview.html:73
#: allianceauth/templates/allianceauth/admin-status/overview.html:65
msgid "Task Queue"
msgstr "Warteschlange"
#: allianceauth/templates/allianceauth/admin-status/overview.html:90
#: allianceauth/templates/allianceauth/admin-status/overview.html:82
msgid "Error retrieving task queue length"
msgstr "Fehler beim Ermitteln der Warteschlange."
#: allianceauth/templates/allianceauth/admin-status/overview.html:92
#: allianceauth/templates/allianceauth/admin-status/overview.html:84
#, python-format
msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-08 00:57+0000\n"
"POT-Creation-Date: 2020-07-29 04:56+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -26,55 +26,53 @@ msgstr ""
msgid "Email"
msgstr ""
#: allianceauth/authentication/models.py:76
msgid "State Changed"
#: allianceauth/authentication/models.py:78
#, python-format
msgid "State changed to: %s"
msgstr ""
#: allianceauth/authentication/models.py:77
#: allianceauth/authentication/models.py:79
#, python-format
msgid "Your user state has been changed to %(state)s"
msgid "Your user's state is now: %(state)s"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:5
#: allianceauth/authentication/templates/authentication/dashboard.html:8
#: allianceauth/templates/allianceauth/side-menu.html:10
#: allianceauth/templates/allianceauth/side-menu.html:12
msgid "Dashboard"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:17
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
#: allianceauth/authentication/templates/authentication/dashboard.html:18
#, python-format
msgid ""
"\n"
" Main Character (State: %(state)s)\n"
" "
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:77
#: allianceauth/authentication/templates/authentication/dashboard.html:81
msgid "No main character set."
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:84
#: allianceauth/authentication/templates/authentication/dashboard.html:88
msgid "Add Character"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:88
#: allianceauth/authentication/templates/authentication/dashboard.html:92
msgid "Change Main"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/authentication/templates/authentication/dashboard.html:101
msgid "Group Memberships"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/authentication/templates/authentication/dashboard.html:121
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:125
#: allianceauth/authentication/templates/authentication/dashboard.html:129
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:22
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -83,13 +81,13 @@ msgstr ""
msgid "Name"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:126
#: allianceauth/authentication/templates/authentication/dashboard.html:130
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:127
#: allianceauth/authentication/templates/authentication/dashboard.html:131
#: allianceauth/corputils/templates/corputils/corpstats.html:77
#: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance"
@@ -221,8 +219,8 @@ msgstr ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:28
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:27
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:37
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:96
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:51
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:110
msgid "Character"
msgstr ""
@@ -244,6 +242,16 @@ msgstr ""
msgid "Killboard"
msgstr ""
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr ""
#: allianceauth/corputils/templates/corputils/corpstats.html:117
#: allianceauth/corputils/templates/corputils/search.html:17
msgid "Main Corporation"
@@ -579,8 +587,8 @@ msgid "Portrait"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:30
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:38
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:52
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:111
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:23
msgid "Organization"
msgstr ""
@@ -599,7 +607,7 @@ msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:17
msgid "Groups"
msgstr ""
@@ -643,7 +651,7 @@ msgid "Audit Members"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:56
msgid "Copy Direrct Join Link"
msgid "Copy Direct Join Link"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:68
@@ -675,37 +683,37 @@ msgstr ""
msgid "Groups Management"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:25
msgid "Join Requests"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:33
msgid "Leave Requests"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:39
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:98
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:53
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:20
#: allianceauth/services/modules/openfire/forms.py:6
msgid "Group"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:71
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:130
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:85
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:144
msgid "Accept"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:74
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:133
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:88
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:147
#: allianceauth/hrapplications/templates/hrapplications/view.html:85
msgid "Reject"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:83
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
msgid "No group add requests."
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:142
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:156
msgid "No group leave requests."
msgstr ""
@@ -714,7 +722,7 @@ msgid "Toggle navigation"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:23
#: allianceauth/templates/allianceauth/side-menu.html:25
msgid "Group Management"
msgstr ""
@@ -726,70 +734,70 @@ msgstr ""
msgid "Group Membership"
msgstr ""
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:162
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:164
msgid "User does not exist in that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:171
#: allianceauth/groupmanagement/views.py:167
msgid "Group does not exist"
msgstr ""
#: allianceauth/groupmanagement/views.py:198
#: allianceauth/groupmanagement/views.py:194
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#: allianceauth/groupmanagement/views.py:201
#: allianceauth/groupmanagement/views.py:234
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
"%(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:231
#: allianceauth/groupmanagement/views.py:227
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:267
#: allianceauth/groupmanagement/views.py:263
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#: allianceauth/groupmanagement/views.py:269
#: allianceauth/groupmanagement/views.py:303
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:300
#: allianceauth/groupmanagement/views.py:296
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:342
#: allianceauth/groupmanagement/views.py:354
msgid "You cannot join that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:352
#: allianceauth/groupmanagement/views.py:348
msgid "You are already a member of that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:363
msgid "You already have a pending application for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/groupmanagement/views.py:366
#: allianceauth/groupmanagement/views.py:404
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -801,24 +809,24 @@ msgstr ""
msgid "Pending"
msgstr ""
#: allianceauth/groupmanagement/views.py:376
#: allianceauth/groupmanagement/views.py:372
#, python-format
msgid "Applied to group %(group)s."
msgstr ""
#: allianceauth/groupmanagement/views.py:387
#: allianceauth/groupmanagement/views.py:383
msgid "You cannot leave that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:392
#: allianceauth/groupmanagement/views.py:388
msgid "You are not a member of that group"
msgstr ""
#: allianceauth/groupmanagement/views.py:401
#: allianceauth/groupmanagement/views.py:397
msgid "You already have a pending leave request for that group."
msgstr ""
#: allianceauth/groupmanagement/views.py:414
#: allianceauth/groupmanagement/views.py:410
#, python-format
msgid "Applied to leave group %(group)s."
msgstr ""
@@ -1281,22 +1289,49 @@ msgstr ""
msgid "Password must be at least 8 characters long."
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:23
#: allianceauth/services/modules/discord/models.py:224
msgid "Discord Account Disabled"
msgstr ""
#: allianceauth/services/modules/discord/models.py:226
msgid ""
"Your Discord account was disabeled automatically by Auth. If you think this "
"was a mistake, please contact an admin."
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:18
msgid "Join the Discord server"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:22
msgid "Leave- and rejoin the Discord Server (Reset)"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:25
msgid "Leave the Discord server"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32
msgid "Link Discord Server"
msgstr ""
#: allianceauth/services/modules/discord/views.py:26
#: allianceauth/services/modules/discord/views.py:30
msgid "Deactivated Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
#: allianceauth/services/modules/discord/views.py:36
#: allianceauth/services/modules/discord/views.py:59
msgid "An error occurred while processing your Discord account."
msgstr ""
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
#: allianceauth/services/modules/discord/views.py:102
msgid "Your Discord account has been successfully activated."
msgstr ""
#: allianceauth/services/modules/discord/views.py:108
msgid ""
"An error occurred while trying to activate your Discord account. Please try "
"again."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:37
@@ -1848,32 +1883,30 @@ msgid "Current"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:40
msgid "Latest Major"
msgid "Latest Stable"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:46
#: allianceauth/templates/allianceauth/admin-status/overview.html:56
#: allianceauth/templates/allianceauth/admin-status/overview.html:66
msgid "Update available"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:50
msgid "Latest Minor"
#: allianceauth/templates/allianceauth/admin-status/overview.html:51
msgid "Latest Pre-Release"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:60
msgid "Latest Patch"
#: allianceauth/templates/allianceauth/admin-status/overview.html:57
msgid "Pre-Release available"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:73
#: allianceauth/templates/allianceauth/admin-status/overview.html:65
msgid "Task Queue"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:90
#: allianceauth/templates/allianceauth/admin-status/overview.html:82
msgid "Error retrieving task queue length"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:92
#: allianceauth/templates/allianceauth/admin-status/overview.html:84
#, python-format
msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-08 00:57+0000\n"
"POT-Creation-Date: 2020-07-29 04:56+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Alexander Gess <de.alex.gess@gmail.com>, 2020\n"
"Language-Team: Russian (https://www.transifex.com/alliance-auth/teams/107430/ru/)\n"
@@ -29,55 +29,56 @@ msgstr "Необходимо указать основного персонаж
msgid "Email"
msgstr "Email"
#: allianceauth/authentication/models.py:76
msgid "State Changed"
msgstr "Состояние заменено. "
#: allianceauth/authentication/models.py:77
#: allianceauth/authentication/models.py:78
#, python-format
msgid "Your user state has been changed to %(state)s"
msgstr "Статус вашего пользователя сменен на %(state)s"
msgid "State changed to: %s"
msgstr "Статус изменен: %s"
#: allianceauth/authentication/models.py:79
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr "Статус пилота: %(state)s"
#: allianceauth/authentication/templates/authentication/dashboard.html:5
#: allianceauth/authentication/templates/authentication/dashboard.html:8
#: allianceauth/templates/allianceauth/side-menu.html:10
#: allianceauth/templates/allianceauth/side-menu.html:12
msgid "Dashboard"
msgstr "Панель показателей"
#: allianceauth/authentication/templates/authentication/dashboard.html:17
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "Основной персонаж"
#: allianceauth/authentication/templates/authentication/dashboard.html:18
#, python-format
msgid ""
"\n"
" Main Character (State: %(state)s)\n"
" "
msgstr ""
"\n"
" Основной персонаж (статус: %(state)s)\n"
" "
#: allianceauth/authentication/templates/authentication/dashboard.html:77
#: allianceauth/authentication/templates/authentication/dashboard.html:81
msgid "No main character set."
msgstr "Основной персонаж не установлен."
#: allianceauth/authentication/templates/authentication/dashboard.html:84
#: allianceauth/authentication/templates/authentication/dashboard.html:88
msgid "Add Character"
msgstr "Добавить Персонажа"
#: allianceauth/authentication/templates/authentication/dashboard.html:88
#: allianceauth/authentication/templates/authentication/dashboard.html:92
msgid "Change Main"
msgstr "Сменить основного персонажа"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/authentication/templates/authentication/dashboard.html:101
msgid "Group Memberships"
msgstr "Групповое участие"
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/authentication/templates/authentication/dashboard.html:121
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters"
msgstr "Персонажи"
#: allianceauth/authentication/templates/authentication/dashboard.html:125
#: allianceauth/authentication/templates/authentication/dashboard.html:129
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:22
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -86,13 +87,13 @@ msgstr "Персонажи"
msgid "Name"
msgstr "Имя"
#: allianceauth/authentication/templates/authentication/dashboard.html:126
#: allianceauth/authentication/templates/authentication/dashboard.html:130
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp"
msgstr "Корпорация"
#: allianceauth/authentication/templates/authentication/dashboard.html:127
#: allianceauth/authentication/templates/authentication/dashboard.html:131
#: allianceauth/corputils/templates/corputils/corpstats.html:77
#: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance"
@@ -141,6 +142,7 @@ msgid ""
"Cannot change main character to %(char)s: character owned by a different "
"account."
msgstr ""
"Нельзя сменить основного персонажа на %(char)s: похоже, что Владелец не Вы. "
#: allianceauth/authentication/views.py:80
#, python-format
@@ -226,8 +228,8 @@ msgstr "Последнее обновление: "
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:28
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:27
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:37
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:96
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:51
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:110
msgid "Character"
msgstr "Персонаж"
@@ -249,6 +251,16 @@ msgstr "Корпорация"
msgid "Killboard"
msgstr "zKillBoard"
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "Основной персонаж"
#: allianceauth/corputils/templates/corputils/corpstats.html:117
#: allianceauth/corputils/templates/corputils/search.html:17
msgid "Main Corporation"
@@ -588,8 +600,8 @@ msgid "Portrait"
msgstr "Портрет"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:30
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:38
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:52
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:111
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:23
msgid "Organization"
msgstr "Корпорация"
@@ -608,7 +620,7 @@ msgstr "Участники группы"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:17
msgid "Groups"
msgstr "Группы"
@@ -652,8 +664,8 @@ msgid "Audit Members"
msgstr "Проверить участников"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:56
msgid "Copy Direrct Join Link"
msgstr ""
msgid "Copy Direct Join Link"
msgstr "Скопировать ссылку подключения"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:68
msgid "No groups to list."
@@ -684,37 +696,37 @@ msgstr "Нет доступных групп."
msgid "Groups Management"
msgstr "Управление Группами"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:25
msgid "Join Requests"
msgstr "Запрос на присоединение"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:33
msgid "Leave Requests"
msgstr "Запрос на Выход"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:39
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:98
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:53
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:20
#: allianceauth/services/modules/openfire/forms.py:6
msgid "Group"
msgstr "Группа"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:71
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:130
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:85
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:144
msgid "Accept"
msgstr "Принять"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:74
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:133
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:88
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:147
#: allianceauth/hrapplications/templates/hrapplications/view.html:85
msgid "Reject"
msgstr "Сбросить"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:83
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
msgid "No group add requests."
msgstr "Нет групповых запросов на вступление"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:142
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:156
msgid "No group leave requests."
msgstr "Нет групповых запросов на выход"
@@ -723,7 +735,7 @@ msgid "Toggle navigation"
msgstr "Проложить маршрут"
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:23
#: allianceauth/templates/allianceauth/side-menu.html:25
msgid "Group Management"
msgstr "Управление Группой"
@@ -735,26 +747,26 @@ msgstr "Групповой запрос"
msgid "Group Membership"
msgstr "Групповое участие"
#: allianceauth/groupmanagement/views.py:166
#: allianceauth/groupmanagement/views.py:162
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "Пользователь %(user)s исключен из %(group)s."
#: allianceauth/groupmanagement/views.py:168
#: allianceauth/groupmanagement/views.py:164
msgid "User does not exist in that group"
msgstr "Пользователь не существует в этой группе."
#: allianceauth/groupmanagement/views.py:171
#: allianceauth/groupmanagement/views.py:167
msgid "Group does not exist"
msgstr "Группа не существует."
#: allianceauth/groupmanagement/views.py:198
#: allianceauth/groupmanagement/views.py:194
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "Запрос от %(mainchar)sв %(group)s принят."
#: allianceauth/groupmanagement/views.py:205
#: allianceauth/groupmanagement/views.py:238
#: allianceauth/groupmanagement/views.py:201
#: allianceauth/groupmanagement/views.py:234
#, python-format
msgid ""
"An unhandled error occurred while processing the application from "
@@ -763,44 +775,46 @@ msgstr ""
"Персонаж %(mainchar)s не может быть добавлен %(group)s, из-за непредвиденной"
" ошибки. "
#: allianceauth/groupmanagement/views.py:231
#: allianceauth/groupmanagement/views.py:227
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s исключен из %(group)s."
#: allianceauth/groupmanagement/views.py:267
#: allianceauth/groupmanagement/views.py:263
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "Утвержден выход %(mainchar)s из %(group)s. "
#: allianceauth/groupmanagement/views.py:273
#: allianceauth/groupmanagement/views.py:307
#: allianceauth/groupmanagement/views.py:269
#: allianceauth/groupmanagement/views.py:303
#, 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:300
#: allianceauth/groupmanagement/views.py:296
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "Прошение об исключении %(mainchar)s из %(group)s отклонено. "
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:358
#: allianceauth/groupmanagement/views.py:342
#: allianceauth/groupmanagement/views.py:354
msgid "You cannot join that group"
msgstr "Вы не можете вступить"
#: allianceauth/groupmanagement/views.py:352
#: allianceauth/groupmanagement/views.py:348
msgid "You are already a member of that group."
msgstr ""
msgstr "Вы уже участник этой группы."
#: allianceauth/groupmanagement/views.py:367
#: allianceauth/groupmanagement/views.py:363
msgid "You already have a pending application for that group."
msgstr ""
msgstr "Вы уже подали заявку на вступление этой группы."
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/groupmanagement/views.py:366
#: allianceauth/groupmanagement/views.py:404
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -812,24 +826,24 @@ msgstr ""
msgid "Pending"
msgstr "Ожидание"
#: allianceauth/groupmanagement/views.py:376
#: allianceauth/groupmanagement/views.py:372
#, python-format
msgid "Applied to group %(group)s."
msgstr "Вступить в группу %(group)s."
#: allianceauth/groupmanagement/views.py:387
#: allianceauth/groupmanagement/views.py:383
msgid "You cannot leave that group"
msgstr "Вы не можете покинуть эту группу"
#: allianceauth/groupmanagement/views.py:392
#: allianceauth/groupmanagement/views.py:388
msgid "You are not a member of that group"
msgstr "Вы не участник группыы"
#: allianceauth/groupmanagement/views.py:401
#: allianceauth/groupmanagement/views.py:397
msgid "You already have a pending leave request for that group."
msgstr ""
msgstr "Ваш запрос находится на рассмотрении"
#: allianceauth/groupmanagement/views.py:414
#: allianceauth/groupmanagement/views.py:410
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "Запрос на выход из группы %(group)s."
@@ -1222,15 +1236,15 @@ msgstr "Состояния"
#: allianceauth/services/abstract.py:72
msgid "That service account already exists"
msgstr ""
msgstr "Этот сервис уже активирован"
#: allianceauth/services/abstract.py:104
msgid "Successfully set your {} password"
msgstr ""
msgstr "{} Пароль успешно обновлен."
#: allianceauth/services/auth_hooks.py:11
msgid "Services"
msgstr ""
msgstr "Сервисные услуги"
#: allianceauth/services/forms.py:6
msgid "Name of Fleet:"
@@ -1292,37 +1306,74 @@ msgstr "Пароль"
msgid "Password must be at least 8 characters long."
msgstr "Пароль должен быть не менее 8 символов."
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:23
#: allianceauth/services/modules/discord/models.py:224
msgid "Discord Account Disabled"
msgstr "Discord персонаж отключен"
#: allianceauth/services/modules/discord/models.py:226
msgid ""
"Your Discord account was disabeled automatically by Auth. If you think this "
"was a mistake, please contact an admin."
msgstr ""
"Ваш доступ на сервер Discord был отменен. Если Вы считаете что по ошибке, "
"пожалуйста, свяжитесь с СЕО."
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:18
msgid "Join the Discord server"
msgstr "Подключиться к серверу Discord"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:22
msgid "Leave- and rejoin the Discord Server (Reset)"
msgstr "Переподключиться к серверу Discord. "
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:25
msgid "Leave the Discord server"
msgstr "Покинуть Discord сервер"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32
msgid "Link Discord Server"
msgstr "Ссылка на сервер Discord"
#: allianceauth/services/modules/discord/views.py:26
#: allianceauth/services/modules/discord/views.py:30
msgid "Deactivated Discord account."
msgstr ""
msgstr "Отменить доступ на Discord сервер."
#: allianceauth/services/modules/discord/views.py:29
#: allianceauth/services/modules/discord/views.py:41
#: allianceauth/services/modules/discord/views.py:65
#: allianceauth/services/modules/discord/views.py:36
#: allianceauth/services/modules/discord/views.py:59
msgid "An error occurred while processing your Discord account."
msgstr ""
"Во время обработки Discord аккаунта возникла ошибка. Попробуйте чуточку "
"позднее. "
#: allianceauth/services/modules/discord/views.py:62
msgid "Activated Discord account."
#: allianceauth/services/modules/discord/views.py:102
msgid "Your Discord account has been successfully activated."
msgstr "Доступ на сервер Discord успешно получен."
#: allianceauth/services/modules/discord/views.py:108
msgid ""
"An error occurred while trying to activate your Discord account. Please try "
"again."
msgstr ""
"Во время активации Discord аккаунта возникла ошибка. Попробуйте чуточку "
"позднее. "
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
msgstr ""
msgstr "Вы не авторизованы в Discourse."
#: allianceauth/services/modules/discourse/views.py:42
msgid "You must have a main character set to access Discourse."
msgstr ""
"Для авторизации Discourse, необходимо получить авторизацию Вашим основным "
"аккаунтом."
#: allianceauth/services/modules/discourse/views.py:52
msgid ""
"No SSO payload or signature. Please contact support if this problem "
"persists."
msgstr ""
"Отсуствует связь SSO. Если ошибка повторяется - свяжитесь с тех. поддержкой."
" "
#: allianceauth/services/modules/discourse/views.py:62
#: allianceauth/services/modules/discourse/views.py:70
@@ -1354,7 +1405,7 @@ msgstr ""
#: allianceauth/services/modules/openfire/auth_hooks.py:26
msgid "Jabber"
msgstr ""
msgstr "Jabber"
#: allianceauth/services/modules/openfire/auth_hooks.py:78
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:6
@@ -1380,50 +1431,50 @@ msgstr "Бродкаст"
#: allianceauth/services/modules/openfire/views.py:35
msgid "Activated jabber account."
msgstr ""
msgstr "Активировать доступ в jabber."
#: allianceauth/services/modules/openfire/views.py:44
#: allianceauth/services/modules/openfire/views.py:57
#: allianceauth/services/modules/openfire/views.py:78
#: allianceauth/services/modules/openfire/views.py:151
msgid "An error occurred while processing your jabber account."
msgstr ""
msgstr "Возникла ошибка во время активации jabber'а ."
#: allianceauth/services/modules/openfire/views.py:70
msgid "Reset jabber password."
msgstr ""
msgstr "Сбросить jabber пароль."
#: allianceauth/services/modules/openfire/views.py:119
#, python-format
msgid "Sent jabber broadcast to %s"
msgstr ""
msgstr "Отправить Бродкаст %s"
#: allianceauth/services/modules/openfire/views.py:148
msgid "Set jabber password."
msgstr ""
msgstr "Установить jabber пароль."
#: allianceauth/services/modules/phpbb3/views.py:34
msgid "Activated forum account."
msgstr ""
msgstr "Допустить на Форум."
#: allianceauth/services/modules/phpbb3/views.py:43
#: allianceauth/services/modules/phpbb3/views.py:57
#: allianceauth/services/modules/phpbb3/views.py:80
#: allianceauth/services/modules/phpbb3/views.py:103
msgid "An error occurred while processing your forum account."
msgstr ""
msgstr "Во время обработки Форумного аккаунта, возникла ошибка."
#: allianceauth/services/modules/phpbb3/views.py:54
msgid "Deactivated forum account."
msgstr ""
msgstr "Отменить доступ на Форум. "
#: allianceauth/services/modules/phpbb3/views.py:71
msgid "Reset forum password."
msgstr ""
msgstr "Сбросить пароль на Форум."
#: allianceauth/services/modules/phpbb3/views.py:100
msgid "Set forum password."
msgstr ""
msgstr "Установить пароль на Форум."
#: allianceauth/services/modules/smf/views.py:34
msgid "Activated SMF account."
@@ -1473,21 +1524,21 @@ msgstr "Продолжить"
#: allianceauth/services/modules/teamspeak3/views.py:34
msgid "Activated TeamSpeak3 account."
msgstr ""
msgstr "Активировать аккаунт TeamSpeak3."
#: allianceauth/services/modules/teamspeak3/views.py:37
#: allianceauth/services/modules/teamspeak3/views.py:74
#: allianceauth/services/modules/teamspeak3/views.py:100
msgid "An error occurred while processing your TeamSpeak3 account."
msgstr ""
msgstr "Во время активации TeamSpeak3 возникла ошибка, попробуйте позже."
#: allianceauth/services/modules/teamspeak3/views.py:71
msgid "Deactivated TeamSpeak3 account."
msgstr ""
msgstr "Отключить TeamSpeak3 аккаунт."
#: allianceauth/services/modules/teamspeak3/views.py:97
msgid "Reset TeamSpeak3 permission key."
msgstr ""
msgstr "Сбросить TeamSpeak3 ключ доступа."
#: allianceauth/services/modules/xenforo/views.py:30
msgid "Activated XenForo account."
@@ -1866,32 +1917,30 @@ msgid "Current"
msgstr "Текущий"
#: allianceauth/templates/allianceauth/admin-status/overview.html:40
msgid "Latest Major"
msgstr "Последняя версия"
msgid "Latest Stable"
msgstr "Стабильная Версия"
#: allianceauth/templates/allianceauth/admin-status/overview.html:46
#: allianceauth/templates/allianceauth/admin-status/overview.html:56
#: allianceauth/templates/allianceauth/admin-status/overview.html:66
msgid "Update available"
msgstr "Доступно обновление"
#: allianceauth/templates/allianceauth/admin-status/overview.html:50
msgid "Latest Minor"
msgstr "Последняя версия"
#: allianceauth/templates/allianceauth/admin-status/overview.html:51
msgid "Latest Pre-Release"
msgstr "Предрелизная Версия"
#: allianceauth/templates/allianceauth/admin-status/overview.html:60
msgid "Latest Patch"
msgstr "Последние исправления"
#: allianceauth/templates/allianceauth/admin-status/overview.html:57
msgid "Pre-Release available"
msgstr "Предрелизная Версия"
#: allianceauth/templates/allianceauth/admin-status/overview.html:73
#: allianceauth/templates/allianceauth/admin-status/overview.html:65
msgid "Task Queue"
msgstr "Список задач"
#: allianceauth/templates/allianceauth/admin-status/overview.html:90
#: allianceauth/templates/allianceauth/admin-status/overview.html:82
msgid "Error retrieving task queue length"
msgstr "Ошибка при получении списка задач. "
#: allianceauth/templates/allianceauth/admin-status/overview.html:92
#: allianceauth/templates/allianceauth/admin-status/overview.html:84
#, python-format
msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks"

View File

@@ -6,15 +6,16 @@
# Translators:
# Joel Falknau <ozirascal@gmail.com>, 2020
# Jesse . <sgeine@hotmail.com>, 2020
# Aaron BuBu <351793078@qq.com>, 2020
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-10 01:32+0000\n"
"POT-Creation-Date: 2020-07-29 03:24+0000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Jesse . <sgeine@hotmail.com>, 2020\n"
"Last-Translator: Aaron BuBu <351793078@qq.com>, 2020\n"
"Language-Team: Chinese Simplified (https://www.transifex.com/alliance-auth/teams/107430/zh-Hans/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -30,55 +31,53 @@ msgstr "只有主要角色才能执行这个操作。在下面添加一个"
msgid "Email"
msgstr "电子邮箱"
#: allianceauth/authentication/models.py:76
msgid "State Changed"
msgstr "状态已经更改"
#: allianceauth/authentication/models.py:77
#: allianceauth/authentication/models.py:78
#, python-format
msgid "Your user state has been changed to %(state)s"
msgstr "您的用户状态已经更改为%(state)s"
msgid "State changed to: %s"
msgstr ""
#: allianceauth/authentication/models.py:79
#, python-format
msgid "Your user's state is now: %(state)s"
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:5
#: allianceauth/authentication/templates/authentication/dashboard.html:8
#: allianceauth/templates/allianceauth/side-menu.html:10
#: allianceauth/templates/allianceauth/side-menu.html:12
msgid "Dashboard"
msgstr "账户总览"
#: allianceauth/authentication/templates/authentication/dashboard.html:17
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "主要角色"
#: allianceauth/authentication/templates/authentication/dashboard.html:18
#, python-format
msgid ""
"\n"
" Main Character (State: %(state)s)\n"
" "
msgstr ""
#: allianceauth/authentication/templates/authentication/dashboard.html:77
#: allianceauth/authentication/templates/authentication/dashboard.html:81
msgid "No main character set."
msgstr "没有主要角色组"
#: allianceauth/authentication/templates/authentication/dashboard.html:84
#: allianceauth/authentication/templates/authentication/dashboard.html:88
msgid "Add Character"
msgstr "添加角色"
#: allianceauth/authentication/templates/authentication/dashboard.html:88
#: allianceauth/authentication/templates/authentication/dashboard.html:92
msgid "Change Main"
msgstr "修改主要角色"
#: allianceauth/authentication/templates/authentication/dashboard.html:97
#: allianceauth/authentication/templates/authentication/dashboard.html:101
msgid "Group Memberships"
msgstr "用户组成员"
#: allianceauth/authentication/templates/authentication/dashboard.html:117
#: allianceauth/authentication/templates/authentication/dashboard.html:121
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:41
msgid "Characters"
msgstr "角色"
#: allianceauth/authentication/templates/authentication/dashboard.html:125
#: allianceauth/authentication/templates/authentication/dashboard.html:129
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:73
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:22
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:15
@@ -87,13 +86,13 @@ msgstr "角色"
msgid "Name"
msgstr "角色名"
#: allianceauth/authentication/templates/authentication/dashboard.html:126
#: allianceauth/authentication/templates/authentication/dashboard.html:130
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp"
msgstr "所在公司"
#: allianceauth/authentication/templates/authentication/dashboard.html:127
#: allianceauth/authentication/templates/authentication/dashboard.html:131
#: allianceauth/corputils/templates/corputils/corpstats.html:77
#: allianceauth/hrapplications/templates/hrapplications/view.html:47
msgid "Alliance"
@@ -134,40 +133,47 @@ msgstr "您的IT团队"
msgid "Submit"
msgstr "提交"
#: allianceauth/authentication/views.py:77
#: allianceauth/authentication/views.py:74
#, python-format
msgid ""
"Cannot change main character to %(char)s: character owned by a different "
"account."
msgstr "不能修改主角色为%(char)s这个角色被另一个账户所拥有"
#: allianceauth/authentication/views.py:80
#, python-format
msgid "Changed main character to %(char)s"
msgstr "修改主要角色为%(char)s"
#: allianceauth/authentication/views.py:86
#: allianceauth/authentication/views.py:89
#, python-format
msgid "Added %(name)s to your account."
msgstr "添加%(name)s到您的账户"
#: allianceauth/authentication/views.py:88
#: allianceauth/authentication/views.py:91
#, python-format
msgid "Failed to add %(name)s to your account: they already have an account."
msgstr "添加%(name)s到您的账户失败他们已经在一个账户中了"
#: allianceauth/authentication/views.py:127
#: allianceauth/authentication/views.py:130
msgid "Unable to authenticate as the selected character."
msgstr "无法作为选定的角色进行身份验证"
#: allianceauth/authentication/views.py:145
#: allianceauth/authentication/views.py:148
msgid "Registration token has expired."
msgstr "注册令牌过期。"
#: allianceauth/authentication/views.py:197
#: allianceauth/authentication/views.py:200
msgid ""
"Sent confirmation email. Please follow the link to confirm your email "
"address."
msgstr "已经发送了确认邮件。请按照链接确定您的电邮地址"
#: allianceauth/authentication/views.py:202
#: allianceauth/authentication/views.py:205
msgid "Confirmed your email address. Please login to continue."
msgstr "已确认您的电邮地址。请登录以继续"
#: allianceauth/authentication/views.py:207
#: allianceauth/authentication/views.py:210
msgid "Registraion of new accounts it not allowed at this time."
msgstr "现在不允许注册新账户。"
@@ -218,8 +224,8 @@ msgstr "最后一次更新"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:28
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:27
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:29
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:37
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:96
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:51
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:110
msgid "Character"
msgstr "角色"
@@ -241,6 +247,16 @@ msgstr "公司"
msgid "Killboard"
msgstr "KB板"
#: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:16
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:22
#: allianceauth/hrapplications/templates/hrapplications/management.html:83
#: allianceauth/hrapplications/templates/hrapplications/management.html:128
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:25
#: allianceauth/hrapplications/templates/hrapplications/view.html:32
msgid "Main Character"
msgstr "主要角色"
#: allianceauth/corputils/templates/corputils/corpstats.html:117
#: allianceauth/corputils/templates/corputils/search.html:17
msgid "Main Corporation"
@@ -527,6 +543,12 @@ msgstr "PAP链接已过期"
msgid "Audit Log"
msgstr "审计日志"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:18
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:20
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "返回"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:25
msgid "Date/Time"
msgstr "日期/时间"
@@ -568,8 +590,8 @@ msgid "Portrait"
msgstr "人物头像"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembers.html:30
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:38
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:52
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:111
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:23
msgid "Organization"
msgstr "组织"
@@ -586,6 +608,12 @@ msgstr "用户组里没人呀,你叫我怎么列"
msgid "Groups Membership"
msgstr "用户组成员"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:14
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:17
msgid "Groups"
msgstr "群组"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/groups.html:16
msgid "Description"
@@ -625,7 +653,11 @@ msgstr "查看成员"
msgid "Audit Members"
msgstr "编辑成员"
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:64
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:56
msgid "Copy Direct Join Link"
msgstr ""
#: allianceauth/groupmanagement/templates/groupmanagement/groupmembership.html:68
msgid "No groups to list."
msgstr "无可用组"
@@ -654,37 +686,37 @@ msgstr "没有可用用户组"
msgid "Groups Management"
msgstr "用户组管理"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:23
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:25
msgid "Join Requests"
msgstr "入组的请求"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:24
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:33
msgid "Leave Requests"
msgstr "离组的请求"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:39
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:98
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:53
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:112
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:20
#: allianceauth/services/modules/openfire/forms.py:6
msgid "Group"
msgstr "用户组"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:71
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:130
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:85
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:144
msgid "Accept"
msgstr "接受"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:74
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:133
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:88
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:147
#: allianceauth/hrapplications/templates/hrapplications/view.html:85
msgid "Reject"
msgstr "拒绝"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:83
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:97
msgid "No group add requests."
msgstr "没有加入用户组的请求,小老弟你是不是摇不到人"
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:142
#: allianceauth/groupmanagement/templates/groupmanagement/index.html:156
msgid "No group leave requests."
msgstr "没有离开用户组的请求,小老弟你人缘可以啊?"
@@ -693,7 +725,7 @@ msgid "Toggle navigation"
msgstr "打开导航栏"
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:15
#: allianceauth/templates/allianceauth/side-menu.html:23
#: allianceauth/templates/allianceauth/side-menu.html:25
msgid "Group Management"
msgstr "用户组管理"
@@ -705,61 +737,70 @@ msgstr "用户组请求"
msgid "Group Membership"
msgstr "用户组成员"
#: allianceauth/groupmanagement/views.py:165
#: allianceauth/groupmanagement/views.py:162
#, python-format
msgid "Removed user %(user)s from group %(group)s."
msgstr "已将用户%(user)s从用户组%(group)s中移除"
#: allianceauth/groupmanagement/views.py:167
#: allianceauth/groupmanagement/views.py:164
msgid "User does not exist in that group"
msgstr "那个用户组中不存在这个用户"
#: allianceauth/groupmanagement/views.py:170
#: allianceauth/groupmanagement/views.py:167
msgid "Group does not exist"
msgstr "用户组不存在"
#: allianceauth/groupmanagement/views.py:197
#: allianceauth/groupmanagement/views.py:194
#, python-format
msgid "Accepted application from %(mainchar)s to %(group)s."
msgstr "已接受用户%(mainchar)s加入%(group)s的申请"
#: allianceauth/groupmanagement/views.py:204
#: allianceauth/groupmanagement/views.py:237
#: allianceauth/groupmanagement/views.py:201
#: allianceauth/groupmanagement/views.py:234
#, 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:230
#: allianceauth/groupmanagement/views.py:227
#, python-format
msgid "Rejected application from %(mainchar)s to %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已拒绝"
#: allianceauth/groupmanagement/views.py:266
#: allianceauth/groupmanagement/views.py:263
#, python-format
msgid "Accepted application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s加入%(group)s的申请已通过"
#: allianceauth/groupmanagement/views.py:272
#: allianceauth/groupmanagement/views.py:306
#: allianceauth/groupmanagement/views.py:269
#: allianceauth/groupmanagement/views.py:303
#, python-format
msgid ""
"An unhandled error occured while processing the application from "
"An unhandled error occurred while processing the application from "
"%(mainchar)s to leave %(group)s."
msgstr "在处理%(mainchar)s离开%(group)s的请求时发生了搞不定的错误"
msgstr "在处理%(mainchar)s离开%(group)s的程序时发生了未知的错误"
#: allianceauth/groupmanagement/views.py:299
#: allianceauth/groupmanagement/views.py:296
#, python-format
msgid "Rejected application from %(mainchar)s to leave %(group)s."
msgstr "%(mainchar)s离开%(group)s的请求已被拒绝"
#: allianceauth/groupmanagement/views.py:346
#: allianceauth/groupmanagement/views.py:342
#: allianceauth/groupmanagement/views.py:354
msgid "You cannot join that group"
msgstr "你无法加入那个用户组"
#: allianceauth/groupmanagement/views.py:370
#: allianceauth/groupmanagement/views.py:408
#: allianceauth/groupmanagement/views.py:348
msgid "You are already a member of that group."
msgstr "你已经是那个群组的一员了。"
#: allianceauth/groupmanagement/views.py:363
msgid "You already have a pending application for that group."
msgstr "你已经有了该组的未决申请"
#: allianceauth/groupmanagement/views.py:366
#: allianceauth/groupmanagement/views.py:404
#: allianceauth/hrapplications/templates/hrapplications/management.html:37
#: allianceauth/hrapplications/templates/hrapplications/management.html:72
#: allianceauth/hrapplications/templates/hrapplications/management.html:99
@@ -771,20 +812,24 @@ msgstr "你无法加入那个用户组"
msgid "Pending"
msgstr "待定"
#: allianceauth/groupmanagement/views.py:376
#: allianceauth/groupmanagement/views.py:372
#, python-format
msgid "Applied to group %(group)s."
msgstr "修改已经应用到%(group)s啦"
#: allianceauth/groupmanagement/views.py:387
#: allianceauth/groupmanagement/views.py:383
msgid "You cannot leave that group"
msgstr "你无法离开那个用户组"
#: allianceauth/groupmanagement/views.py:392
#: allianceauth/groupmanagement/views.py:388
msgid "You are not a member of that group"
msgstr "你不是那个用户组的成员"
#: allianceauth/groupmanagement/views.py:414
#: allianceauth/groupmanagement/views.py:397
msgid "You already have a pending leave request for that group."
msgstr "你已经有了该组的未决离开请求"
#: allianceauth/groupmanagement/views.py:410
#, python-format
msgid "Applied to leave group %(group)s."
msgstr "已经离开群组%(group)s"
@@ -1130,10 +1175,6 @@ msgstr "对搞事时间节点%(opname)s的修改保存了朝令夕改你是
msgid "Permissions Audit"
msgstr "放行记录审计"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
msgid "Back"
msgstr "返回"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:22
msgid "User / Character"
msgstr "用户/角色"
@@ -1175,15 +1216,22 @@ msgstr "操作类型"
msgid "Users"
msgstr "用户"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:40
#: allianceauth/templates/allianceauth/side-menu.html:15
msgid "Groups"
msgstr "群组"
#: allianceauth/permissions_tool/templates/permissions_tool/overview.html:43
msgid "States"
msgstr "声望"
#: allianceauth/services/abstract.py:72
msgid "That service account already exists"
msgstr "该服务账户仍然存在"
#: allianceauth/services/abstract.py:104
msgid "Successfully set your {} password"
msgstr "成功修改了你的[]密码"
#: allianceauth/services/auth_hooks.py:11
msgid "Services"
msgstr "服务"
#: allianceauth/services/forms.py:6
msgid "Name of Fleet:"
msgstr "舰队名称"
@@ -1244,19 +1292,111 @@ msgstr "密码"
msgid "Password must be at least 8 characters long."
msgstr "密码至少要有8个字符啊你也太不注重安全啦"
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:23
#: allianceauth/services/modules/discord/models.py:224
msgid "Discord Account Disabled"
msgstr ""
#: allianceauth/services/modules/discord/models.py:226
msgid ""
"Your Discord account was disabeled automatically by Auth. If you think this "
"was a mistake, please contact an admin."
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:18
msgid "Join the Discord server"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:22
msgid "Leave- and rejoin the Discord Server (Reset)"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:25
msgid "Leave the Discord server"
msgstr ""
#: allianceauth/services/modules/discord/templates/services/discord/discord_service_ctrl.html:32
msgid "Link Discord Server"
msgstr "链接到Discord服务器"
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "消息"
#: allianceauth/services/modules/discord/views.py:30
msgid "Deactivated Discord account."
msgstr "停用Discord账户"
#: allianceauth/services/modules/discord/views.py:36
#: allianceauth/services/modules/discord/views.py:59
msgid "An error occurred while processing your Discord account."
msgstr "在处理你的Discord账户时出错。"
#: allianceauth/services/modules/discord/views.py:102
msgid "Your Discord account has been successfully activated."
msgstr ""
#: allianceauth/services/modules/discord/views.py:108
msgid ""
"An error occurred while trying to activate your Discord account. Please try "
"again."
msgstr ""
#: allianceauth/services/modules/discourse/views.py:37
msgid "You are not authorized to access Discourse."
msgstr "你无权访问Discourse"
#: allianceauth/services/modules/discourse/views.py:42
msgid "You must have a main character set to access Discourse."
msgstr "你必须得有一个主要角色来访问Discourse"
#: allianceauth/services/modules/discourse/views.py:52
msgid ""
"No SSO payload or signature. Please contact support if this problem "
"persists."
msgstr "没有在Seat上检测到SSO。如果该问题依然存在请联系技术支持"
#: allianceauth/services/modules/discourse/views.py:62
#: allianceauth/services/modules/discourse/views.py:70
msgid "Invalid payload. Please contact support if this problem persists."
msgstr "无效的SSO验证。如果该问题依然存在请联系技术支持。"
#: allianceauth/services/modules/ips4/views.py:31
msgid "Activated IPSuite4 account."
msgstr "完成激活IPSuite4账户"
#: allianceauth/services/modules/ips4/views.py:40
#: allianceauth/services/modules/ips4/views.py:62
#: allianceauth/services/modules/ips4/views.py:83
#: allianceauth/services/modules/ips4/views.py:103
msgid "An error occurred while processing your IPSuite4 account."
msgstr "在处理你的IPSuite4账户时出错"
#: allianceauth/services/modules/ips4/views.py:53
msgid "Reset IPSuite4 password."
msgstr "重置IPSuite4密码"
#: allianceauth/services/modules/ips4/views.py:80
msgid "Set IPSuite4 password."
msgstr "修改IPSuite4密码"
#: allianceauth/services/modules/ips4/views.py:100
msgid "Deactivated IPSuite4 account."
msgstr "停用IPSuite4账户"
#: allianceauth/services/modules/openfire/auth_hooks.py:26
msgid "Jabber"
msgstr "Jabber"
#: allianceauth/services/modules/openfire/auth_hooks.py:78
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:6
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:11
msgid "Jabber Broadcast"
msgstr "Jabber广播"
#: allianceauth/services/modules/openfire/auth_hooks.py:91
msgid "Fleet Broadcast Formatter"
msgstr "舰队广播设置"
#: allianceauth/services/modules/openfire/forms.py:7
msgid "Message"
msgstr "消息"
#: allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html:17
msgid "Broadcast Sent!!"
msgstr "广播出去了!"
@@ -1265,6 +1405,76 @@ msgstr "广播出去了!"
msgid "Broadcast"
msgstr "广播"
#: allianceauth/services/modules/openfire/views.py:35
msgid "Activated jabber account."
msgstr "成功激活jabber账户"
#: allianceauth/services/modules/openfire/views.py:44
#: allianceauth/services/modules/openfire/views.py:57
#: allianceauth/services/modules/openfire/views.py:78
#: allianceauth/services/modules/openfire/views.py:151
msgid "An error occurred while processing your jabber account."
msgstr "在处理你的jabber账户时出错"
#: allianceauth/services/modules/openfire/views.py:70
msgid "Reset jabber password."
msgstr "重置jabber密码"
#: allianceauth/services/modules/openfire/views.py:119
#, python-format
msgid "Sent jabber broadcast to %s"
msgstr "已经将jabber广播送到了%s"
#: allianceauth/services/modules/openfire/views.py:148
msgid "Set jabber password."
msgstr "修改jabber密码"
#: allianceauth/services/modules/phpbb3/views.py:34
msgid "Activated forum account."
msgstr "成功激活论坛账户"
#: allianceauth/services/modules/phpbb3/views.py:43
#: allianceauth/services/modules/phpbb3/views.py:57
#: allianceauth/services/modules/phpbb3/views.py:80
#: allianceauth/services/modules/phpbb3/views.py:103
msgid "An error occurred while processing your forum account."
msgstr "在处理你的论坛账户时发生了一个错误"
#: allianceauth/services/modules/phpbb3/views.py:54
msgid "Deactivated forum account."
msgstr "停用论坛账户"
#: allianceauth/services/modules/phpbb3/views.py:71
msgid "Reset forum password."
msgstr "重置论坛密码"
#: allianceauth/services/modules/phpbb3/views.py:100
msgid "Set forum password."
msgstr "修改论坛密码"
#: allianceauth/services/modules/smf/views.py:34
msgid "Activated SMF account."
msgstr "成功激活SMF论坛账户"
#: allianceauth/services/modules/smf/views.py:43
#: allianceauth/services/modules/smf/views.py:58
#: allianceauth/services/modules/smf/views.py:80
#: allianceauth/services/modules/smf/views.py:103
msgid "An error occurred while processing your SMF account."
msgstr "在处理你的SMF论坛账户时发生了一个错误"
#: allianceauth/services/modules/smf/views.py:55
msgid "Deactivated SMF account."
msgstr "停用SMF论坛账户"
#: allianceauth/services/modules/smf/views.py:72
msgid "Reset SMF password."
msgstr "重置SMF密码"
#: allianceauth/services/modules/smf/views.py:100
msgid "Set SMF password."
msgstr "修改SMF论坛密码"
#: allianceauth/services/modules/teamspeak3/forms.py:14
#, python-format
msgid "Unable to locate user %s on server"
@@ -1288,6 +1498,47 @@ msgstr "加入服务器"
msgid "Continue"
msgstr "下一个"
#: allianceauth/services/modules/teamspeak3/views.py:34
msgid "Activated TeamSpeak3 account."
msgstr "成功激活TeamSpeak3账户"
#: allianceauth/services/modules/teamspeak3/views.py:37
#: allianceauth/services/modules/teamspeak3/views.py:74
#: allianceauth/services/modules/teamspeak3/views.py:100
msgid "An error occurred while processing your TeamSpeak3 account."
msgstr "在处理你的TeamSpeak3账户时发生了错误"
#: allianceauth/services/modules/teamspeak3/views.py:71
msgid "Deactivated TeamSpeak3 account."
msgstr "停用TeamSpeak3账户"
#: allianceauth/services/modules/teamspeak3/views.py:97
msgid "Reset TeamSpeak3 permission key."
msgstr "重置TeamSpeak3授权秘钥"
#: allianceauth/services/modules/xenforo/views.py:30
msgid "Activated XenForo account."
msgstr "成功激活XenForo账户"
#: allianceauth/services/modules/xenforo/views.py:40
#: allianceauth/services/modules/xenforo/views.py:52
#: allianceauth/services/modules/xenforo/views.py:73
#: allianceauth/services/modules/xenforo/views.py:94
msgid "An error occurred while processing your XenForo account."
msgstr "在处理你的XenForo账户时发生了一个错误"
#: allianceauth/services/modules/xenforo/views.py:50
msgid "Deactivated XenForo account."
msgstr "停用XenForo论坛账户"
#: allianceauth/services/modules/xenforo/views.py:65
msgid "Reset XenForo account password."
msgstr "重置XenForo密码"
#: allianceauth/services/modules/xenforo/views.py:91
msgid "Changed XenForo password."
msgstr "修改XenForo密码"
#: allianceauth/services/templates/services/fleetformattertool.html:6
msgid "Fleet Formatter Tool"
msgstr "起队工具"
@@ -1638,43 +1889,35 @@ msgid "Current"
msgstr "当前版本"
#: allianceauth/templates/allianceauth/admin-status/overview.html:40
msgid "Latest Major"
msgstr "最新主版本号"
msgid "Latest Stable"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:46
#: allianceauth/templates/allianceauth/admin-status/overview.html:56
#: allianceauth/templates/allianceauth/admin-status/overview.html:66
msgid "Update available"
msgstr "有更新!"
#: allianceauth/templates/allianceauth/admin-status/overview.html:50
msgid "Latest Minor"
msgstr "最新小版本号"
#: allianceauth/templates/allianceauth/admin-status/overview.html:51
msgid "Latest Pre-Release"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:60
msgid "Latest Patch"
msgstr "最新补丁版本"
#: allianceauth/templates/allianceauth/admin-status/overview.html:57
msgid "Pre-Release available"
msgstr ""
#: allianceauth/templates/allianceauth/admin-status/overview.html:73
#: allianceauth/templates/allianceauth/admin-status/overview.html:65
msgid "Task Queue"
msgstr "任务队列"
#: allianceauth/templates/allianceauth/admin-status/overview.html:90
#: allianceauth/templates/allianceauth/admin-status/overview.html:82
msgid "Error retrieving task queue length"
msgstr "获取任务队列长度的时候出错啦!"
#: allianceauth/templates/allianceauth/admin-status/overview.html:92
#: allianceauth/templates/allianceauth/admin-status/overview.html:84
#, python-format
msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks"
msgstr[0] "%(tasks)s个任务"
#: allianceauth/templates/allianceauth/help.html:4
#: allianceauth/templates/allianceauth/help.html:9
#: allianceauth/templates/allianceauth/side-menu.html:34
msgid "Help"
msgstr "帮助"
#: allianceauth/templates/allianceauth/night-toggle.html:3
msgid "Night"
msgstr "暗色皮肤"

View File

@@ -33,7 +33,8 @@ class DiscordService(ServicesHook):
if self.user_has_account(user):
logger.debug('Deleting user %s %s account', user, self.name)
tasks.delete_user.apply_async(
kwargs={'user_pk': user.pk}, priority=SINGLE_TASK_PRIORITY
kwargs={'user_pk': user.pk, 'notify_user': notify_user},
priority=SINGLE_TASK_PRIORITY
)
def render_services_ctrl(self, request):
@@ -60,13 +61,21 @@ class DiscordService(ServicesHook):
)
def service_active_for_user(self, user):
return user.has_perm(self.access_perm)
has_perms = user.has_perm(self.access_perm)
logger.debug("User %s has service permission: %s", user, has_perms)
return has_perms
def sync_nickname(self, user):
logger.debug('Syncing %s nickname for user %s', self.name, user)
if self.user_has_account(user):
tasks.update_nickname.apply_async(
kwargs={'user_pk': user.pk}, priority=SINGLE_TASK_PRIORITY
kwargs={
'user_pk': user.pk,
# since the new nickname is not yet in the DB we need to
# provide it manually to the task
'nickname': DiscordUser.objects.user_formatted_nick(user)
},
priority=SINGLE_TASK_PRIORITY
)
def sync_nicknames_bulk(self, users: list):
@@ -84,10 +93,16 @@ class DiscordService(ServicesHook):
tasks.update_all_groups.delay()
def update_groups(self, user):
logger.debug('Processing %s groups for %s', self.name, user)
if self.user_has_account(user):
logger.debug('Processing %s groups for %s', self.name, user)
if self.user_has_account(user):
tasks.update_groups.apply_async(
kwargs={'user_pk': user.pk}, priority=SINGLE_TASK_PRIORITY
kwargs={
'user_pk': user.pk,
# since state changes may not yet be in the DB we need to
# provide the new state name manually to the task
'state_name': user.profile.state.name
},
priority=SINGLE_TASK_PRIORITY
)
def update_groups_bulk(self, users: list):

View File

@@ -6,9 +6,14 @@ DISCORD_API_BASE_URL = clean_setting(
'DISCORD_API_BASE_URL', 'https://discordapp.com/api/'
)
# Low level timeout for requests to the Discord API in ms
DISCORD_API_TIMEOUT = clean_setting(
'DISCORD_API_TIMEOUT', 5000
# Low level connecttimeout for requests to the Discord API in seconds
DISCORD_API_TIMEOUT_CONNECT = clean_setting(
'DISCORD_API_TIMEOUT', 5
)
# Low level read timeout for requests to the Discord API in seconds
DISCORD_API_TIMEOUT_READ = clean_setting(
'DISCORD_API_TIMEOUT', 30
)
# Base authorization URL for Discord Oauth
@@ -22,14 +27,14 @@ DISCORD_OAUTH_TOKEN_URL = clean_setting(
)
# How long the Discord guild names retrieved from the server are
# caches locally in milliseconds.
# caches locally in seconds.
DISCORD_GUILD_NAME_CACHE_MAX_AGE = clean_setting(
'DISCORD_GUILD_NAME_CACHE_MAX_AGE', 3600 * 1 * 1000
'DISCORD_GUILD_NAME_CACHE_MAX_AGE', 3600 * 24
)
# How long Discord roles retrieved from the server are caches locally in milliseconds.
# How long Discord roles retrieved from the server are caches locally in seconds.
DISCORD_ROLES_CACHE_MAX_AGE = clean_setting(
'DISCORD_ROLES_CACHE_MAX_AGE', 3600 * 1 * 1000
'DISCORD_ROLES_CACHE_MAX_AGE', 3600 * 1
)
# Turns off creation of new roles. In case the rate limit for creating roles is

View File

@@ -15,7 +15,8 @@ from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
from .. import __title__
from .app_settings import (
DISCORD_API_BASE_URL,
DISCORD_API_TIMEOUT,
DISCORD_API_TIMEOUT_CONNECT,
DISCORD_API_TIMEOUT_READ,
DISCORD_DISABLE_ROLE_CREATION,
DISCORD_GUILD_NAME_CACHE_MAX_AGE,
DISCORD_OAUTH_BASE_URL,
@@ -179,12 +180,19 @@ class DiscordClient:
r = self._api_request(method='get', route=route)
return r.json()
def guild_name(self, guild_id: int) -> str:
def guild_name(self, guild_id: int, use_cache: bool = True) -> str:
"""returns the name of this guild (cached)
or an empty string if something went wrong
Params:
- guild_id: ID of current guild
- use_cache: When set to False will force an API call to get the server name
"""
key_name = self._guild_name_cache_key(guild_id)
guild_name = self._redis_decode(self._redis.get(key_name))
if use_cache:
guild_name = self._redis_decode(self._redis.get(key_name))
else:
guild_name = None
if not guild_name:
guild_infos = self.guild_infos(guild_id)
if 'name' in guild_infos:
@@ -192,7 +200,7 @@ class DiscordClient:
self._redis.set(
name=key_name,
value=guild_name,
px=DISCORD_GUILD_NAME_CACHE_MAX_AGE
ex=DISCORD_GUILD_NAME_CACHE_MAX_AGE
)
else:
guild_name = ''
@@ -229,7 +237,7 @@ class DiscordClient:
self._redis.set(
name=cache_key,
value=json.dumps(roles),
px=DISCORD_ROLES_CACHE_MAX_AGE
ex=DISCORD_ROLES_CACHE_MAX_AGE
)
return roles
@@ -273,6 +281,11 @@ class DiscordClient:
gen_key = cls._generate_hash(f'{guild_id}')
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
def match_role_from_name(self, guild_id: int, role_name: str) -> dict:
"""returns Discord role matching the given name or an empty dict"""
guild_roles = DiscordRoles(self.guild_roles(guild_id))
return guild_roles.role_by_name(role_name)
def match_or_create_roles_from_names(self, guild_id: int, role_names: list) -> list:
"""returns Discord roles matching the given names
@@ -280,6 +293,7 @@ class DiscordClient:
Will try to match with existing roles names
Non-existing roles will be created, then created flag will be True
Params:
- guild_id: ID of guild
- role_names: list of name strings each defining a role
@@ -310,6 +324,7 @@ class DiscordClient:
Will try to match with existing roles names
Non-existing roles will be created, then created flag will be True
Params:
- guild_id: ID of guild
- role_name: strings defining name of a role
@@ -540,7 +555,7 @@ class DiscordClient:
args = {
'url': url,
'headers': headers,
'timeout': DISCORD_API_TIMEOUT / 1000
'timeout': (DISCORD_API_TIMEOUT_CONNECT, DISCORD_API_TIMEOUT_READ)
}
if data:
args['json'] = data

View File

@@ -280,6 +280,8 @@ class TestGuildGetName(TestCase):
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_name(TEST_GUILD_ID)
self.assertEqual(result, guild_name)
self.assertTrue(my_mock_redis.get.called)
self.assertFalse(my_mock_redis.set.called)
@patch(MODULE_PATH + '.DiscordClient.guild_infos')
def test_fetches_from_server_if_not_found_in_cache_and_stores_in_cache(
@@ -291,6 +293,20 @@ class TestGuildGetName(TestCase):
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_name(TEST_GUILD_ID)
self.assertEqual(result, guild_name)
self.assertTrue(my_mock_redis.get.called)
self.assertTrue(my_mock_redis.set.called)
@patch(MODULE_PATH + '.DiscordClient.guild_infos')
def test_fetches_from_server_if_asked_to_ignore_cache_and_stores_in_cache(
self, mock_guild_get_infos
):
guild_name = 'Omega'
my_mock_redis = MagicMock(**{'get.return_value': False})
mock_guild_get_infos.return_value = {'id': TEST_GUILD_ID, 'name': guild_name}
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_name(TEST_GUILD_ID, use_cache=False)
self.assertFalse(my_mock_redis.get.called)
self.assertEqual(result, guild_name)
self.assertTrue(my_mock_redis.set.called)
@patch(MODULE_PATH + '.DiscordClient.guild_infos')
@@ -302,6 +318,7 @@ class TestGuildGetName(TestCase):
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_name(TEST_GUILD_ID)
self.assertEqual(result, '')
self.assertTrue(my_mock_redis.get.called)
self.assertFalse(my_mock_redis.set.called)
@@ -844,9 +861,45 @@ class TestGuildMemberRemoveRole(TestCase):
self.assertFalse(result)
class TestMatchGuildRolesToName(TestCase):
def setUp(self):
self.url = f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles'
@requests_mock.Mocker()
def test_return_role_if_known(self, requests_mocker):
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=ALL_ROLES
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.match_role_from_name(TEST_GUILD_ID, "alpha")
self.assertDictEqual(result, ROLE_ALPHA)
@requests_mock.Mocker()
def test_return_empty_dict_if_not_known(self, requests_mocker):
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=ALL_ROLES
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.match_role_from_name(TEST_GUILD_ID, "unknown")
self.assertDictEqual(result, dict())
@patch(MODULE_PATH + '.DiscordClient.create_guild_role')
@patch(MODULE_PATH + '.DiscordClient.guild_roles')
class TestMatchGuildRolesToName(TestCase):
class TestMatchOrCreateGuildRolesToName(TestCase):
def test_return_role_if_known(
self, mock_guild_get_roles, mock_guild_create_role,
@@ -896,7 +949,7 @@ class TestMatchGuildRolesToName(TestCase):
@patch(MODULE_PATH + '.DiscordClient.create_guild_role')
@patch(MODULE_PATH + '.DiscordClient.guild_roles')
class TestMatchGuildRolesToNames(TestCase):
class TestMatchOrCreateGuildRolesToNames(TestCase):
def test_return_roles_if_known(
self, mock_guild_get_roles, mock_guild_create_role,

View File

@@ -4,7 +4,7 @@ from urllib.parse import urlencode
from requests_oauthlib import OAuth2Session
from requests.exceptions import HTTPError
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from django.db import models
from django.utils.timezone import now
@@ -19,7 +19,8 @@ from .app_settings import (
DISCORD_GUILD_ID,
DISCORD_SYNC_NAMES
)
from .discord_client import DiscordClient, DiscordApiBackoff
from .discord_client import DiscordClient
from .discord_client.exceptions import DiscordClientException, DiscordApiBackoff
from .discord_client.helpers import match_or_create_roles_from_names
from .utils import LoggerAddTag
@@ -127,9 +128,17 @@ class DiscordUserManager(models.Manager):
return None
@staticmethod
def user_group_names(user: User) -> list:
def user_group_names(user: User, state_name: str = None) -> list:
"""returns list of group names plus state the given user is a member of"""
return [group.name for group in user.groups.all()] + [user.profile.state.name]
if not state_name:
state_name = user.profile.state.name
group_names = (
[group.name for group in user.groups.all()] + [state_name]
)
logger.debug(
"Group names for roles updates of user %s are: %s", user, group_names
)
return group_names
def user_has_account(self, user: User) -> bool:
"""Returns True if the user has an Discord account, else False
@@ -141,7 +150,7 @@ class DiscordUserManager(models.Manager):
return self.filter(user=user).select_related('user').exists()
@classmethod
def generate_bot_add_url(cls):
def generate_bot_add_url(cls) -> str:
params = urlencode({
'client_id': DISCORD_APP_ID,
'scope': 'bot',
@@ -151,7 +160,7 @@ class DiscordUserManager(models.Manager):
return f'{DiscordClient.OAUTH_BASE_URL}?{params}'
@classmethod
def generate_oauth_redirect_url(cls):
def generate_oauth_redirect_url(cls) -> str:
oauth = OAuth2Session(
DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL, scope=cls.SCOPES
)
@@ -170,11 +179,38 @@ class DiscordUserManager(models.Manager):
return token['access_token']
@classmethod
def server_name(cls):
"""returns the name of the Discord server"""
return cls._bot_client().guild_name(DISCORD_GUILD_ID)
def server_name(cls, use_cache: bool = True) -> str:
"""returns the name of the current Discord server
or an empty string if the name could not be retrieved
Params:
- use_cache: When set False will force an API call to get the server name
"""
try:
server_name = cls._bot_client().guild_name(
guild_id=DISCORD_GUILD_ID, use_cache=use_cache
)
except (HTTPError, DiscordClientException):
server_name = ""
except Exception:
logger.warning(
"Unexpected error when trying to retrieve the server name from Discord",
exc_info=True
)
server_name = ""
return server_name
@classmethod
def group_to_role(cls, group: Group) -> dict:
"""returns the Discord role matching the given Django group by name
or an empty dict() if no matching role exist
"""
return cls._bot_client().match_role_from_name(
guild_id=DISCORD_GUILD_ID, role_name=group.name
)
@staticmethod
def _bot_client(is_rate_limited: bool = True):
def _bot_client(is_rate_limited: bool = True) -> DiscordClient:
"""returns a bot client for access to the Discord API"""
return DiscordClient(DISCORD_BOT_TOKEN, is_rate_limited=is_rate_limited)

View File

@@ -67,21 +67,25 @@ class DiscordUser(models.Model):
def __repr__(self):
return f'{type(self).__name__}(user=\'{self.user}\', uid={self.uid})'
def update_nickname(self) -> bool:
def update_nickname(self, nickname: str = None) -> bool:
"""Update nickname with formatted name of main character
Params:
- nickname: optional nickname to be used instead of user's main
Returns:
- True on success
- None if user is no longer a member of the Discord server
- False on error or raises exception
"""
requested_nick = DiscordUser.objects.user_formatted_nick(self.user)
if requested_nick:
if not nickname:
nickname = DiscordUser.objects.user_formatted_nick(self.user)
if nickname:
client = DiscordUser.objects._bot_client()
success = client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=self.uid,
nick=requested_nick
nick=nickname
)
if success:
logger.info('Nickname for %s has been updated', self.user)
@@ -92,10 +96,13 @@ class DiscordUser(models.Model):
else:
return False
def update_groups(self) -> bool:
def update_groups(self, state_name: str = None) -> bool:
"""update groups for a user based on his current group memberships.
Will add or remove roles of a user as needed.
Params:
- state_name: optional state name to be used
Returns:
- True on success
- None if user is no longer a member of the Discord server
@@ -128,7 +135,9 @@ class DiscordUser(models.Model):
requested_roles = match_or_create_roles_from_names(
client=client,
guild_id=DISCORD_GUILD_ID,
role_names=DiscordUser.objects.user_group_names(self.user)
role_names=DiscordUser.objects.user_group_names(
user=self.user, state_name=state_name
)
)
logger.debug(
'Requested roles for user %s: %s', self.user, requested_roles.ids()
@@ -144,13 +153,13 @@ class DiscordUser(models.Model):
role_ids=list(new_roles.ids())
)
if success:
logger.info('Groups for %s have been updated', self.user)
logger.info('Roles for %s have been updated', self.user)
else:
logger.warning('Failed to update groups for %s', self.user)
logger.warning('Failed to update roles for %s', self.user)
return success
else:
logger.info('No need to update groups for user %s', self.user)
logger.info('No need to update roles for user %s', self.user)
return True
def update_username(self) -> bool:

View File

@@ -1,4 +1,5 @@
import logging
from typing import Any
from celery import shared_task, chain
from requests.exceptions import HTTPError
@@ -26,25 +27,27 @@ BULK_TASK_PRIORITY = 6
@shared_task(
bind=True, name='discord.update_groups', base=QueueOnce, max_retries=None
)
def update_groups(self, user_pk: int) -> None:
def update_groups(self, user_pk: int, state_name: str = None) -> None:
"""Update roles on Discord for given user according to his current groups
Params:
- user_pk: PK of given user
"""
_task_perform_user_action(self, user_pk, 'update_groups')
- state_name: optional state name to be used
"""
_task_perform_user_action(self, user_pk, 'update_groups', state_name=state_name)
@shared_task(
bind=True, name='discord.update_nickname', base=QueueOnce, max_retries=None
)
def update_nickname(self, user_pk: int) -> None:
def update_nickname(self, user_pk: int, nickname: str = None) -> None:
"""Set nickname on Discord for given user to his main character name
Params:
- user_pk: PK of given user
- nickname: optional nickname to be used instead of user's main
"""
_task_perform_user_action(self, user_pk, 'update_nickname')
_task_perform_user_action(self, user_pk, 'update_nickname', nickname=nickname)
@shared_task(
@@ -75,6 +78,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
"""perform a user related action incl. managing all exceptions"""
logger.debug("Starting %s for user with pk %s", method, user_pk)
user = User.objects.get(pk=user_pk)
# logger.debug("user %s has state %s", user, user.profile.state)
if DiscordUser.objects.user_has_account(user):
logger.info("Running %s for user %s", method, user)
try:
@@ -91,7 +95,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
raise self.retry(countdown=bo.retry_after_seconds)
except AttributeError:
raise ValueError(f'{method} not a valid method for DiscordUser: %r')
raise ValueError(f'{method} not a valid method for DiscordUser')
except (HTTPError, ConnectionError):
logger.warning(
@@ -112,7 +116,7 @@ def _task_perform_user_action(self, user_pk: int, method: str, **kwargs) -> None
)
except Exception:
logger.error(
'%s for %s failed due to unexpected exception',
'%s for user %s failed due to unexpected exception',
method,
user,
exc_info=True
@@ -183,9 +187,58 @@ def _bulk_update_nicknames_for_users(discord_users_qs: QuerySet) -> None:
chain(update_nicknames_chain).apply_async(priority=BULK_TASK_PRIORITY)
def _task_perform_users_action(self, method: str, **kwargs) -> Any:
"""Perform an action that concerns a group of users or the whole server
and that hits the API
"""
result = None
try:
result = getattr(DiscordUser.objects, method)(**kwargs)
except AttributeError:
raise ValueError(f'{method} not a valid method for DiscordUser.objects')
except DiscordApiBackoff as bo:
logger.info(
"API back off for %s due to %r, retrying in %s seconds",
method,
bo,
bo.retry_after_seconds
)
raise self.retry(countdown=bo.retry_after_seconds)
except (HTTPError, ConnectionError):
logger.warning(
'%s failed, retrying in %d secs',
method,
DISCORD_TASKS_RETRY_PAUSE,
exc_info=True
)
if self.request.retries < DISCORD_TASKS_MAX_RETRIES:
raise self.retry(countdown=DISCORD_TASKS_RETRY_PAUSE)
else:
logger.error('%s failed after max retries', method, exc_info=True)
except Exception:
logger.error('%s failed due to unexpected exception', method, exc_info=True)
return result
@shared_task(
bind=True, name='discord.update_servername', base=QueueOnce, max_retries=None
)
def update_servername(self) -> None:
"""Updates the Discord server name"""
_task_perform_users_action(self, method="server_name", use_cache=False)
@shared_task(name='discord.update_all_usernames')
def update_all_usernames() -> None:
"""Update all usernames for all known users with a Discord account."""
"""Update all usernames for all known users with a Discord account.
Also updates the server name
"""
update_servername.delay()
discord_users_qs = DiscordUser.objects.all()
_bulk_update_usernames_for_users(discord_users_qs)

View File

@@ -28,7 +28,7 @@
{% endif %}
{% if request.user.is_superuser %}
<div class="text-center" style="padding-top:5px;">
<a type="button" class="btn btn-default" href="{% url 'discord:add_bot' %}">
<a type="button" id="btnLinkDiscordServer" class="btn btn-default" href="{% url 'discord:add_bot' %}">
{% trans "Link Discord Server" %}
</a>
</div>

View File

@@ -10,6 +10,7 @@ from ..discord_client.tests import ( # noqa
ROLE_BRAVO,
ROLE_CHARLIE,
ROLE_MIKE,
ALL_ROLES,
create_user_info
)

View File

@@ -3,6 +3,7 @@ from unittest.mock import patch
from django.test import TestCase, RequestFactory
from django.test.utils import override_settings
from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils
from . import TEST_USER_NAME, TEST_USER_ID, add_permissions_to_members, MODULE_PATH
@@ -30,6 +31,7 @@ class TestDiscordService(TestCase):
self.service = DiscordService
add_permissions_to_members()
self.factory = RequestFactory()
Notification.objects.all().delete()
def test_service_enabled(self):
service = self.service()
@@ -89,16 +91,17 @@ class TestDiscordService(TestCase):
service = self.service()
service.sync_nicknames_bulk([self.member])
self.assertTrue(mock_update_nicknames_bulk.delay.called)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
def test_delete_user_is_member(self, mock_DiscordClient):
def test_delete_user_is_member(self, mock_DiscordClient):
mock_DiscordClient.return_value.remove_guild_member.return_value = True
service = self.service()
service.delete_user(self.member)
service = self.service()
service.delete_user(self.member, notify_user=True)
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
self.assertTrue(Notification.objects.filter(user=self.member).exists())
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
def test_delete_user_is_not_member(self, mock_DiscordClient):

View File

@@ -16,9 +16,12 @@ import requests_mock
from django.contrib.auth.models import Group, User
from django.core.cache import caches
from django.shortcuts import reverse
from django.test import TransactionTestCase
from django.test import TransactionTestCase, TestCase
from django.test.utils import override_settings
from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter
from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils
from . import (
@@ -38,11 +41,14 @@ from . import (
create_user_info
)
from ..discord_client.app_settings import DISCORD_API_BASE_URL
from ..discord_client.exceptions import DiscordApiBackoff
from ..models import DiscordUser
from .. import tasks
logger = logging.getLogger('allianceauth')
ROLE_MEMBER = create_role(99, 'Member')
ROLE_BLUE = create_role(98, 'Blue')
# Putting all requests to Discord into objects so we can compare them better
DiscordRequest = namedtuple('DiscordRequest', ['method', 'url'])
@@ -87,6 +93,16 @@ def clear_cache():
logger.info('Cache flushed')
def reset_testdata():
AuthUtils.disconnect_signals()
Group.objects.all().delete()
User.objects.all().delete()
State.objects.all().delete()
EveCharacter.objects.all().delete()
AuthUtils.connect_signals()
Notification.objects.all().delete()
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.Mocker()
@@ -96,16 +112,26 @@ class TestServiceFeatures(TransactionTestCase):
def setUpClass(cls):
super().setUpClass()
cls.maxDiff = None
def setUp(self):
"""All tests: Given a user with member state,
service permission and active Discord account
"""
clear_cache()
AuthUtils.disconnect_signals()
Group.objects.all().delete()
User.objects.all().delete()
AuthUtils.connect_signals()
self.group_3 = Group.objects.create(name='charlie')
self.user = AuthUtils.create_member(TEST_USER_NAME)
AuthUtils.add_main_character_2(
reset_testdata()
self.group_charlie = Group.objects.create(name='charlie')
# States
self.member_state = AuthUtils.get_member_state()
self.guest_state = AuthUtils.get_guest_state()
self.blue_state = AuthUtils.create_state("Blue", 50)
permission = AuthUtils.get_permission_by_name('discord.access_discord')
self.member_state.permissions.add(permission)
self.blue_state.permissions.add(permission)
# Test user
self.user = AuthUtils.create_user(TEST_USER_NAME)
self.main = AuthUtils.add_main_character_2(
self.user,
TEST_MAIN_NAME,
TEST_MAIN_ID,
@@ -113,60 +139,55 @@ class TestServiceFeatures(TransactionTestCase):
corp_name='test_corp',
corp_ticker='TEST',
disconnect_signals=True
)
self.discord_user = DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
add_permissions_to_members()
)
self.member_state.member_characters.add(self.main)
def test_name_of_main_changes(self, requests_mocker):
# modify_guild_member()
# verify user is a member and has an account
self.user = User.objects.get(pk=self.user.pk)
self.assertEqual(self.user.profile.state, self.member_state)
self.discord_user = DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
def test_when_name_of_main_changes_then_discord_nick_is_updated(
self, requests_mocker
):
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
# changing nick to trigger signals
new_nick = f'Testnick {uuid1().hex}'[:32]
self.user.profile.main_character.character_name = new_nick
self.user.profile.main_character.save()
# Need to have called modify_guild_member two times only
# Once for sync nickname
# Once for change of main character
requests_made = list()
# verify Discord nick was updates
nick_updated = False
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
my_request = DiscordRequest(r.method, r.url)
if my_request == modify_guild_member_request and "nick" in r.json():
nick_updated = True
self.assertEqual(r.json()["nick"], new_nick)
self.assertTrue(nick_updated)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
expected = [modify_guild_member_request, modify_guild_member_request]
self.assertListEqual(requests_made, expected)
def test_name_of_main_changes_but_user_deleted(self, requests_mocker):
# modify_guild_member()
def test_when_name_of_main_changes_and_user_deleted_then_account_is_deleted(
self, requests_mocker
):
requests_mocker.patch(
modify_guild_member_request.url, status_code=404, json={'code': 10007}
)
# remove_guild_member()
)
requests_mocker.delete(remove_guild_member_request.url, status_code=204)
# changing nick to trigger signals
new_nick = f'Testnick {uuid1().hex}'[:32]
self.user.profile.main_character.character_name = new_nick
self.user.profile.main_character.save()
self.assertFalse(DiscordUser.objects.user_has_account(self.user))
# Need to have called modify_guild_member two times only
# Once for sync nickname
# Once for change of main character
requests_made = list()
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
expected = [
modify_guild_member_request,
remove_guild_member_request,
]
self.assertListEqual(requests_made, expected)
# self.assertFalse(DiscordUser.objects.user_has_account(self.user))
def test_name_of_main_changes_but_user_rate_limited(
def test_when_name_of_main_changes_and_and_rate_limited_then_dont_call_api(
self, requests_mocker
):
# modify_guild_member()
):
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
# exhausting rate limit
@@ -183,98 +204,232 @@ class TestServiceFeatures(TransactionTestCase):
self.user.profile.main_character.save()
# should not have called the API
requests_made = list()
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
requests_made = [
DiscordRequest(r.method, r.url) for r in requests_mocker.request_history
]
self.assertListEqual(requests_made, list())
expected = list()
self.assertListEqual(requests_made, expected)
def test_user_demoted_to_guest(self, requests_mocker):
# remove_guild_member()
def test_when_member_is_demoted_to_guest_then_his_account_is_deleted(
self, requests_mocker
):
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
requests_mocker.delete(remove_guild_member_request.url, status_code=204)
self.user.groups.clear()
requests_made = list()
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
# our user is a member and has an account
self.assertTrue(self.user.has_perm('discord.access_discord'))
# now we demote him to guest
self.member_state.member_characters.remove(self.main)
# compare the list of made requests with expected
expected = [remove_guild_member_request]
self.assertListEqual(requests_made, expected)
# verify user is now guest
self.user = User.objects.get(pk=self.user.pk)
self.assertEqual(self.user.profile.state, AuthUtils.get_guest_state())
# verify user has no longer access to Discord and no account
self.assertFalse(self.user.has_perm('discord.access_discord'))
self.assertFalse(DiscordUser.objects.user_has_account(self.user))
# verify account was actually deleted from Discord server
requests_made = [
DiscordRequest(r.method, r.url) for r in requests_mocker.request_history
]
self.assertIn(remove_guild_member_request, requests_made)
# verify user has been notified
self.assertTrue(Notification.objects.filter(user=self.user).exists())
def test_when_member_changes_to_blue_state_then_roles_are_updated_accordingly(
self, requests_mocker
):
# request mocks
requests_mocker.get(
guild_member_request.url,
json={'user': create_user_info(), 'roles': ['3', '13', '99']}
)
requests_mocker.get(
guild_roles_request.url,
json=[
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_MEMBER, ROLE_BLUE
]
)
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
AuthUtils.disconnect_signals()
self.user.groups.add(self.group_charlie)
AuthUtils.connect_signals()
def test_adding_group_to_user_role_exists(self, requests_mocker):
# guild_member()
# demote user to blue state
self.blue_state.member_characters.add(self.main)
self.member_state.member_characters.remove(self.main)
# verify roles for user where updated
roles_updated = False
for r in requests_mocker.request_history:
my_request = DiscordRequest(r.method, r.url)
if my_request == modify_guild_member_request and "roles" in r.json():
roles_updated = True
self.assertSetEqual(set(r.json()["roles"]), {3, 13, 98})
break
self.assertTrue(roles_updated)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
def test_when_group_added_to_member_and_role_known_then_his_roles_are_updated(
self, requests_mocker
):
requests_mocker.get(
guild_member_request.url,
json={
'user': create_user_info(),
'roles': ['1', '13', '99']
'roles': ['13', '99']
}
)
# guild_roles()
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_MEMBER]
)
# create_guild_role()
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
# modify_guild_member()
)
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
# adding new group to trigger signals
self.user.groups.add(self.group_3)
self.user.refresh_from_db()
# compare the list of made requests with expected
requests_made = list()
self.user.groups.add(self.group_charlie)
# verify roles for user where updated
roles_updated = False
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
my_request = DiscordRequest(r.method, r.url)
if my_request == modify_guild_member_request and "roles" in r.json():
roles_updated = True
self.assertSetEqual(set(r.json()["roles"]), {3, 13, 99})
break
self.assertTrue(roles_updated)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
expected = [
guild_member_request,
guild_roles_request,
modify_guild_member_request
]
self.assertListEqual(requests_made, expected)
def test_adding_group_to_user_role_does_not_exist(self, requests_mocker):
# guild_member()
def test_when_group_added_to_member_and_role_unknown_then_his_roles_are_updated(
self, requests_mocker
):
requests_mocker.get(
guild_member_request.url,
json={
'user': {'id': str(TEST_USER_ID), 'username': TEST_MAIN_NAME},
'roles': ['1', '13', '99']
'roles': ['13', '99']
}
)
# guild_roles()
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER]
)
# create_guild_role()
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
# modify_guild_member()
)
requests_mocker.post(create_guild_role_request.url, json=ROLE_CHARLIE)
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
# adding new group to trigger signals
self.user.groups.add(self.group_3)
self.user.groups.add(self.group_charlie)
self.user.refresh_from_db()
# compare the list of made requests with expected
requests_made = list()
# verify roles for user where updated
roles_updated = False
for r in requests_mocker.request_history:
requests_made.append(DiscordRequest(r.method, r.url))
expected = [
guild_member_request,
guild_roles_request,
create_guild_role_request,
modify_guild_member_request
]
self.assertListEqual(requests_made, expected)
my_request = DiscordRequest(r.method, r.url)
if my_request == modify_guild_member_request and "roles" in r.json():
roles_updated = True
self.assertSetEqual(set(r.json()["roles"]), {3, 13, 99})
break
self.assertTrue(roles_updated)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
@override_settings(CELERY_ALWAYS_EAGER=True)
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
@requests_mock.Mocker()
class StateTestCase(TestCase):
def setUp(self):
clear_cache()
reset_testdata()
self.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(
self.user,
'Perm Test Character', '99',
corp_id='100',
alliance_id='200',
corp_name='Perm Test Corp',
alliance_name='Perm Test Alliance'
)
self.test_character = EveCharacter.objects.get(character_id='99')
self.member_state = State.objects.create(
name='Test Member',
priority=150,
)
self.access_discord = AuthUtils.get_permission_by_name('discord.access_discord')
self.member_state.permissions.add(self.access_discord)
self.member_state.member_characters.add(self.test_character)
def _add_discord_user(self):
self.discord_user = DiscordUser.objects.create(
user=self.user, uid="12345678910"
)
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_perm_changes_to_higher_priority_state_creation(self, requests_mocker):
mock_url = DiscordRequest(
method='DELETE',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/12345678910'
)
requests_mocker.delete(mock_url.url, status_code=204)
self._add_discord_user()
self._refresh_user()
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
self.assertIsNotNone(self.user.discord)
higher_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
with self.assertRaises(DiscordUser.DoesNotExist):
self.user.discord
higher_state.member_characters.clear()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
with self.assertRaises(DiscordUser.DoesNotExist):
self.user.discord
def test_perm_changes_to_lower_priority_state_creation(self, requests_mocker):
mock_url = DiscordRequest(
method='DELETE',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/12345678910'
)
requests_mocker.delete(mock_url.url, status_code=204)
self._add_discord_user()
self._refresh_user()
lower_state = State.objects.create(
name='Lower State',
priority=125,
)
self.assertIsNotNone(self.user.discord)
lower_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear()
self._refresh_user()
self.assertEquals(lower_state, self.user.profile.state)
with self.assertRaises(DiscordUser.DoesNotExist):
self.user.discord
self.member_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
with self.assertRaises(DiscordUser.DoesNotExist):
self.user.discord
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
@requests_mock.Mocker()
@@ -282,6 +437,7 @@ class TestUserFeatures(WebTest):
def setUp(self):
clear_cache()
reset_testdata()
self.member = AuthUtils.create_member(TEST_USER_NAME)
AuthUtils.add_main_character_2(
self.member,
@@ -290,25 +446,26 @@ class TestUserFeatures(WebTest):
disconnect_signals=True
)
add_permissions_to_members()
@patch(MODULE_PATH + '.views.messages')
@patch(MODULE_PATH + '.managers.OAuth2Session')
def test_user_activation_normal(
self, requests_mocker, mock_OAuth2Session, mock_messages
):
# user_get_current()
# setup
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
)
requests_mocker.get(
user_get_current_request.url,
json=create_user_info(
TEST_USER_ID, TEST_USER_NAME, TEST_USER_DISCRIMINATOR
)
)
# guild_roles()
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER]
)
# add_guild_member()
)
requests_mocker.put(add_guild_member_request.url, status_code=201)
authentication_code = 'auth_code'
@@ -320,8 +477,12 @@ class TestUserFeatures(WebTest):
# login
self.app.set_user(self.member)
# click activate on the service page
response = self.app.get(reverse('discord:activate'))
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
# user clicks Discord service activation link on page
response = services_page.click(href=reverse('discord:activate'))
# check we got a redirect to Discord OAuth
self.assertRedirects(
@@ -343,7 +504,10 @@ class TestUserFeatures(WebTest):
requests_made.append(obj)
expected = [
user_get_current_request, guild_roles_request, add_guild_member_request
guild_infos_request,
user_get_current_request,
guild_roles_request,
add_guild_member_request
]
self.assertListEqual(requests_made, expected)
@@ -352,19 +516,21 @@ class TestUserFeatures(WebTest):
def test_user_activation_failed(
self, requests_mocker, mock_OAuth2Session, mock_messages
):
# user_get_current()
# setup
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
)
requests_mocker.get(
user_get_current_request.url,
json=create_user_info(
TEST_USER_ID, TEST_USER_NAME, TEST_USER_DISCRIMINATOR
)
)
# guild_roles()
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER]
)
# add_guild_member()
mock_exception = HTTPError('error')
mock_exception.response = Mock()
mock_exception.response.status_code = 503
@@ -378,9 +544,13 @@ class TestUserFeatures(WebTest):
# login
self.app.set_user(self.member)
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
# click activate on the service page
response = self.app.get(reverse('discord:activate'))
response = services_page.click(href=reverse('discord:activate'))
# check we got a redirect to Discord OAuth
self.assertRedirects(
@@ -402,27 +572,31 @@ class TestUserFeatures(WebTest):
requests_made.append(obj)
expected = [
user_get_current_request, guild_roles_request, add_guild_member_request
guild_infos_request,
user_get_current_request,
guild_roles_request,
add_guild_member_request
]
self.assertListEqual(requests_made, expected)
@patch(MODULE_PATH + '.views.messages')
def test_user_deactivation_normal(self, requests_mocker, mock_messages):
# guild_infos()
# setup
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'})
# remove_guild_member()
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
)
requests_mocker.delete(remove_guild_member_request.url, status_code=204)
# user needs have an account
DiscordUser.objects.create(user=self.member, uid=TEST_USER_ID)
# login
self.app.set_user(self.member)
# click deactivate on the service page
response = self.app.get(reverse('discord:deactivate'))
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
# click deactivate on the service page
response = services_page.click(href=reverse('discord:deactivate'))
# check we got a redirect to service page
self.assertRedirects(response, expected_url=reverse('services:services'))
@@ -436,29 +610,31 @@ class TestUserFeatures(WebTest):
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [remove_guild_member_request, guild_infos_request]
expected = [guild_infos_request, remove_guild_member_request]
self.assertListEqual(requests_made, expected)
@patch(MODULE_PATH + '.views.messages')
def test_user_deactivation_fails(self, requests_mocker, mock_messages):
# guild_infos()
# setup
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'})
# remove_guild_member()
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
)
mock_exception = HTTPError('error')
mock_exception.response = Mock()
mock_exception.response.status_code = 503
requests_mocker.delete(remove_guild_member_request.url, exc=mock_exception)
# user needs have an account
DiscordUser.objects.create(user=self.member, uid=TEST_USER_ID)
# login
self.app.set_user(self.member)
# click deactivate on the service page
response = self.app.get(reverse('discord:deactivate'))
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
# click deactivate on the service page
response = services_page.click(href=reverse('discord:deactivate'))
# check we got a redirect to service page
self.assertRedirects(response, expected_url=reverse('services:services'))
@@ -472,5 +648,60 @@ class TestUserFeatures(WebTest):
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [remove_guild_member_request, guild_infos_request]
expected = [guild_infos_request, remove_guild_member_request]
self.assertListEqual(requests_made, expected)
@patch(MODULE_PATH + '.views.messages')
def test_user_add_new_server(self, requests_mocker, mock_messages):
# setup
mock_exception = HTTPError(Mock(**{"response.status_code": 400}))
requests_mocker.get(guild_infos_request.url, exc=mock_exception)
# login
self.member.is_superuser = True
self.member.is_staff = True
self.member.save()
self.app.set_user(self.member)
# click deactivate on the service page
response = self.app.get(reverse('services:services'))
# check we got can see the page and the "link server" button
self.assertEqual(response.status_int, 200)
self.assertIsNotNone(response.html.find(id='btnLinkDiscordServer'))
def test_when_server_name_fails_user_can_still_see_service_page(
self, requests_mocker
):
# setup
requests_mocker.get(guild_infos_request.url, exc=DiscordApiBackoff(1000))
# login
self.app.set_user(self.member)
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
@override_settings(CELERY_ALWAYS_EAGER=True)
def test_server_name_is_updated_by_task(
self, requests_mocker
):
# setup
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
)
# run task to update usernames
tasks.update_all_usernames()
# login
self.app.set_user(self.member)
# disable API call to make sure server name is not retrieved from API
mock_exception = HTTPError(Mock(**{"response.status_code": 400}))
requests_mocker.get(guild_infos_request.url, exc=mock_exception)
# user opens services page
services_page = self.app.get(reverse('services:services'))
self.assertEqual(services_page.status_code, 200)
self.assertIn("Test Guild", services_page.text)

View File

@@ -17,7 +17,7 @@ from . import (
MODULE_PATH,
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE
ROLE_CHARLIE,
)
from ..discord_client.tests import create_matched_role
from ..app_settings import (
@@ -361,3 +361,61 @@ class TestUserHasAccount(TestCase):
def test_return_false_if_not_called_with_user_object(self):
self.assertFalse(DiscordUser.objects.user_has_account('abc'))
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
@patch(MODULE_PATH + '.managers.logger')
class TestServerName(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = AuthUtils.create_user(TEST_USER_NAME)
def test_returns_name_when_api_returns_it(self, mock_logger, mock_DiscordClient):
server_name = "El Dorado"
mock_DiscordClient.return_value.guild_name.return_value = server_name
self.assertEqual(DiscordUser.objects.server_name(), server_name)
self.assertFalse(mock_logger.warning.called)
def test_returns_empty_string_when_api_throws_http_error(
self, mock_logger, mock_DiscordClient
):
mock_exception = HTTPError('Test exception')
mock_exception.response = Mock(**{"status_code": 440})
mock_DiscordClient.return_value.guild_name.side_effect = mock_exception
self.assertEqual(DiscordUser.objects.server_name(), "")
self.assertFalse(mock_logger.warning.called)
def test_returns_empty_string_when_api_throws_service_error(
self, mock_logger, mock_DiscordClient
):
mock_DiscordClient.return_value.guild_name.side_effect = DiscordApiBackoff(1000)
self.assertEqual(DiscordUser.objects.server_name(), "")
self.assertFalse(mock_logger.warning.called)
def test_returns_empty_string_when_api_throws_unexpected_error(
self, mock_logger, mock_DiscordClient
):
mock_DiscordClient.return_value.guild_name.side_effect = RuntimeError
self.assertEqual(DiscordUser.objects.server_name(), "")
self.assertTrue(mock_logger.warning.called)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
class TestRoleForGroup(TestCase):
def test_return_role_if_found(self, mock_DiscordClient):
mock_DiscordClient.return_value.match_role_from_name.return_value = ROLE_ALPHA
group = Group.objects.create(name='alpha')
self.assertEqual(DiscordUser.objects.group_to_role(group), ROLE_ALPHA)
def test_return_empty_dict_if_not_found(self, mock_DiscordClient):
mock_DiscordClient.return_value.match_role_from_name.return_value = dict()
group = Group.objects.create(name='unknown')
self.assertEqual(DiscordUser.objects.group_to_role(group), dict())

View File

@@ -21,6 +21,7 @@ logger = set_logger_to_file(MODULE_PATH, __file__)
@patch(MODULE_PATH + '.DiscordUser.update_groups')
@patch(MODULE_PATH + ".logger")
class TestUpdateGroups(TestCase):
@classmethod
@@ -32,16 +33,18 @@ class TestUpdateGroups(TestCase):
cls.group_1.user_set.add(cls.user)
cls.group_2.user_set.add(cls.user)
def test_can_update_groups(self, mock_update_groups):
def test_can_update_groups(self, mock_logger, mock_update_groups):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
tasks.update_groups(self.user.pk)
self.assertTrue(mock_update_groups.called)
def test_no_action_if_user_has_no_discord_account(self, mock_update_groups):
def test_no_action_if_user_has_no_discord_account(
self, mock_logger, mock_update_groups
):
tasks.update_groups(self.user.pk)
self.assertFalse(mock_update_groups.called)
def test_retries_on_api_backoff(self, mock_update_groups):
def test_retries_on_api_backoff(self, mock_logger, mock_update_groups):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
mock_exception = DiscordApiBackoff(999)
mock_update_groups.side_effect = mock_exception
@@ -49,7 +52,7 @@ class TestUpdateGroups(TestCase):
with self.assertRaises(Retry):
tasks.update_groups(self.user.pk)
def test_retry_on_http_error_except_404(self, mock_update_groups):
def test_retry_on_http_error_except_404(self, mock_logger, mock_update_groups):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
mock_exception = HTTPError('error')
mock_exception.response = MagicMock()
@@ -58,8 +61,12 @@ class TestUpdateGroups(TestCase):
with self.assertRaises(Retry):
tasks.update_groups(self.user.pk)
self.assertTrue(mock_logger.warning.called)
def test_retry_on_http_error_404_when_user_not_deleted(self, mock_update_groups):
def test_retry_on_http_error_404_when_user_not_deleted(
self, mock_logger, mock_update_groups
):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
mock_exception = HTTPError('error')
mock_exception.response = MagicMock()
@@ -68,26 +75,31 @@ class TestUpdateGroups(TestCase):
with self.assertRaises(Retry):
tasks.update_groups(self.user.pk)
self.assertTrue(mock_logger.warning.called)
def test_retry_on_non_http_error(self, mock_update_groups):
def test_retry_on_non_http_error(self, mock_logger, mock_update_groups):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
mock_update_groups.side_effect = ConnectionError
with self.assertRaises(Retry):
tasks.update_groups(self.user.pk)
self.assertTrue(mock_logger.warning.called)
@patch(MODULE_PATH + '.DISCORD_TASKS_MAX_RETRIES', 3)
def test_log_error_if_retries_exhausted(self, mock_update_groups):
def test_log_error_if_retries_exhausted(self, mock_logger, mock_update_groups):
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
mock_task = MagicMock(**{'request.retries': 3})
mock_update_groups.side_effect = ConnectionError
update_groups_inner = tasks.update_groups.__wrapped__.__func__
update_groups_inner(mock_task, self.user.pk)
self.assertTrue(mock_logger.error.called)
@patch(MODULE_PATH + '.delete_user.delay')
def test_delete_user_if_user_is_no_longer_member_of_discord_server(
self, mock_delete_user, mock_update_groups
self, mock_delete_user, mock_logger, mock_update_groups
):
mock_update_groups.return_value = None
@@ -222,6 +234,72 @@ class TestTaskPerformUserAction(TestCase):
tasks._task_perform_user_action(mock_task, self.user.pk, 'update_groups')
@patch(MODULE_PATH + '.DiscordUser.objects.server_name')
@patch(MODULE_PATH + ".logger")
class TestTaskUpdateServername(TestCase):
def test_normal(self, mock_logger, mock_server_name):
tasks.update_servername()
self.assertTrue(mock_server_name.called)
self.assertFalse(mock_logger.error.called)
_, kwargs = mock_server_name.call_args
self.assertFalse(kwargs["use_cache"])
def test_retries_on_api_backoff(self, mock_logger, mock_server_name):
mock_server_name.side_effect = DiscordApiBackoff(999)
with self.assertRaises(Retry):
tasks.update_servername()
self.assertFalse(mock_logger.error.called)
def test_retry_on_http_error(self, mock_logger, mock_server_name):
mock_exception = HTTPError(MagicMock(**{"response.status_code": 500}))
mock_server_name.side_effect = mock_exception
with self.assertRaises(Retry):
tasks.update_servername()
self.assertTrue(mock_logger.warning.called)
def test_retry_on_connection_error(self, mock_logger, mock_server_name):
mock_server_name.side_effect = ConnectionError
with self.assertRaises(Retry):
tasks.update_servername()
self.assertTrue(mock_logger.warning.called)
@patch(MODULE_PATH + '.DISCORD_TASKS_MAX_RETRIES', 3)
def test_log_error_if_retries_exhausted(self, mock_logger, mock_server_name):
mock_task = MagicMock(**{'request.retries': 3})
mock_server_name.side_effect = ConnectionError
update_groups_inner = tasks.update_servername.__wrapped__.__func__
update_groups_inner(mock_task)
self.assertTrue(mock_logger.error.called)
@patch(MODULE_PATH + '.DiscordUser.objects.server_name')
class TestTaskPerformUsersAction(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_raise_value_error_on_unknown_method(self, mock_server_name):
mock_task = MagicMock(**{'request.retries': 0})
with self.assertRaises(ValueError):
tasks._task_perform_users_action(mock_task, 'invalid_method')
def test_catch_and_log_unexpected_exceptions(self, mock_server_name):
mock_server_name.side_effect = RuntimeError
mock_task = MagicMock(**{'request.retries': 0})
tasks._task_perform_users_action(mock_task, 'server_name')
@override_settings(CELERY_ALWAYS_EAGER=True)
class TestBulkTasks(TestCase):
@@ -299,15 +377,19 @@ class TestBulkTasks(TestCase):
self.assertSetEqual(set(current_pks), set(expected_pks))
@patch(MODULE_PATH + '.update_username.si')
def test_can_update_all_usernames(self, mock_update_username):
@patch(MODULE_PATH + '.update_username')
@patch(MODULE_PATH + '.update_servername')
def test_can_update_all_usernames(
self, mock_update_servername, mock_update_username
):
du_1 = DiscordUser.objects.create(user=self.user_1, uid=123)
du_2 = DiscordUser.objects.create(user=self.user_2, uid=456)
du_3 = DiscordUser.objects.create(user=self.user_3, uid=789)
tasks.update_all_usernames()
self.assertEqual(mock_update_username.call_count, 3)
current_pks = [args[0][0] for args in mock_update_username.call_args_list]
self.assertTrue(mock_update_servername.delay.called)
self.assertEqual(mock_update_username.si.call_count, 3)
current_pks = [args[0][0] for args in mock_update_username.si.call_args_list]
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))

View File

@@ -25,10 +25,10 @@ class ExampleService(ServicesHook):
:return:
"""
urls = self.Urls()
urls.auth_activate = 'auth_example_activate'
urls.auth_deactivate = 'auth_example_deactivate'
urls.auth_reset_password = 'auth_example_reset_password'
urls.auth_set_password = 'auth_example_set_password'
# urls.auth_activate = 'auth_example_activate'
# urls.auth_deactivate = 'auth_example_deactivate'
# urls.auth_reset_password = 'auth_example_reset_password'
# urls.auth_set_password = 'auth_example_set_password'
return render_to_string(self.service_ctrl_template, {
'service_name': self.title,
'urls': urls,

View File

@@ -123,17 +123,12 @@ def m2m_changed_state_permissions(sender, instance, action, pk_set, *args, **kwa
logger.debug("Permission change for state {} was not service permission, ignoring".format(instance))
@receiver(state_changed, sender=UserProfile)
@receiver(state_changed)
def check_service_accounts_state_changed(sender, user, state, **kwargs):
logger.debug("Received state_changed from %s to state %s" % (user, state))
service_perms = [svc.access_perm for svc in ServicesHook.get_services()]
state_perms = ["{}.{}".format(perm.natural_key()[1], perm.natural_key()[0]) for perm in state.permissions.all()]
for perm in service_perms:
if perm not in state_perms:
for svc in ServicesHook.get_services():
if svc.access_perm == perm:
logger.debug("User %s new state %s does not have service %s permission. Checking account." % (user, state, svc))
svc.validate_user(user)
logger.debug("Received state_changed from %s to state %s" % (user, state))
for svc in ServicesHook.get_services():
svc.validate_user(user)
svc.update_groups(user)
@receiver(pre_delete, sender=User)
@@ -159,24 +154,37 @@ def disable_services_on_inactive(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=UserProfile)
def process_main_character_change(sender, instance, *args, **kwargs):
if not instance.pk: # ignore
# new model being created
def process_main_character_change(sender, instance, *args, **kwargs):
if not instance.pk:
# ignore new model being created
return
try:
logger.debug(
"Received pre_save from %s for process_main_character_change", instance
)
old_instance = UserProfile.objects.get(pk=instance.pk)
if old_instance.main_character and not instance.main_character: # lost main char disable services
logger.info("Disabling services due to loss of main character for user {0}".format(instance.user))
if old_instance.main_character and not instance.main_character:
logger.info(
"Disabling services due to loss of main character for user %s",
instance.user
)
disable_user(instance.user)
elif old_instance.main_character is not instance.main_character: # swapping/changing main character
logger.info("Updating Names due to change of main character for user {0}".format(instance.user))
elif old_instance.main_character != instance.main_character:
logger.info(
"Updating Names due to change of main character for user %s",
instance.user
)
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance.user)
svc.sync_nickname(instance.user)
except:
logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance))
logger.exception(
"Exception running sync_nickname for services module %s "
"on user %s",
svc,
instance
)
except UserProfile.DoesNotExist:
pass
@@ -184,8 +192,12 @@ def process_main_character_change(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=EveCharacter)
def process_main_character_update(sender, instance, *args, **kwargs):
try:
try:
if instance.userprofile:
logger.debug(
"Received pre_save from %s for process_main_character_update",
instance
)
old_instance = EveCharacter.objects.get(pk=instance.pk)
if not instance.character_name == old_instance.character_name or \
not instance.corporation_name == old_instance.corporation_name or \

View File

@@ -1,15 +1,20 @@
from copy import deepcopy
from unittest import mock
from django.test import TestCase
from django.contrib.auth.models import Group, Permission
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
class ServicesSignalsTestCase(TestCase):
def setUp(self):
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
AuthUtils.add_main_character(self.member, 'Test', '1', '2', 'Test Corp', 'TEST')
AuthUtils.add_main_character_2(
self.member, 'Test', 1, 2, 'Test Corp', 'TEST'
)
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
@mock.patch('allianceauth.services.signals.transaction')
@@ -46,7 +51,6 @@ class ServicesSignalsTestCase(TestCase):
@mock.patch('allianceauth.services.signals.disable_user')
def test_pre_delete_user(self, disable_user):
"""
Test that disable_member is called when a user is deleted
"""
@@ -126,7 +130,9 @@ class ServicesSignalsTestCase(TestCase):
transaction.on_commit = lambda fn: fn()
ct = ContentType.objects.get(app_label='auth', model='permission')
perm = Permission.objects.create(name="Test perm", codename="access_testsvc", content_type=ct)
perm = Permission.objects.create(
name="Test perm", codename="access_testsvc", content_type=ct
)
self.member.user_permissions.add(perm)
# Act, should trigger m2m change
@@ -159,7 +165,9 @@ class ServicesSignalsTestCase(TestCase):
AuthUtils.connect_signals()
ct = ContentType.objects.get(app_label='auth', model='permission')
perm = Permission.objects.create(name="Test perm", codename="access_testsvc", content_type=ct)
perm = Permission.objects.create(
name="Test perm", codename="access_testsvc", content_type=ct
)
test_state.permissions.add(perm)
# Act, should trigger m2m change
@@ -173,12 +181,12 @@ class ServicesSignalsTestCase(TestCase):
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_state_changed_services_valudation(self, services_hook):
"""
Test a user changing state has service accounts validated
def test_state_changed_services_validation_and_groups_update(self, services_hook):
"""Test a user changing state has service accounts validated and groups updated
"""
svc = mock.Mock()
svc.validate_user.return_value = None
svc.update_groups.return_value = None
svc.access_perm = 'auth.access_testsvc'
services_hook.get_services.return_value = [svc]
@@ -190,6 +198,65 @@ class ServicesSignalsTestCase(TestCase):
# Assert
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
self.assertEqual(self.member, args[0])
self.assertTrue(svc.update_groups.called)
args, kwargs = svc.update_groups.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_state_changed_services_validation_and_groups_update_1(self, services_hook):
"""Test a user changing main has service accounts validated and sync updated
"""
svc = mock.Mock()
svc.validate_user.return_value = None
svc.sync_nickname.return_value = None
svc.access_perm = 'auth.access_testsvc'
services_hook.get_services.return_value = [svc]
new_main = EveCharacter.objects.create(
character_id=123,
character_name="Alter Ego",
corporation_id=987,
corporation_name="ABC"
)
self.member.profile.main_character = new_main
self.member.profile.save()
# Assert
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
self.assertTrue(svc.sync_nickname.called)
args, kwargs = svc.sync_nickname.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_state_changed_services_validation_and_groups_update_2(self, services_hook):
"""Test a user changing main has service does not have accounts validated
and sync updated if the new main is equal to the old main
"""
svc = mock.Mock()
svc.validate_user.return_value = None
svc.sync_nickname.return_value = None
svc.access_perm = 'auth.access_testsvc'
services_hook.get_services.return_value = [svc]
# this creates a clone of the Django object
new_main = deepcopy(self.member.profile.main_character)
self.assertIsNot(new_main, self.member.profile.main_character)
self.member.profile.main_character = new_main
self.member.profile.save()
# Assert
self.assertFalse(services_hook.get_services.called)
self.assertFalse(svc.validate_user.called)
self.assertFalse(svc.sync_nickname.called)

View File

@@ -1,5 +1,4 @@
{% load staticfiles %}
<!-- Font Awesome Bundle -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css" rel="stylesheet" type="text/css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/v4-shims.min.css" rel="stylesheet" type="text/css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" rel="stylesheet" type="text/css">
<!-- End Font Awesome Bundle -->

View File

@@ -33,8 +33,8 @@ GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = (
logger = logging.getLogger(__name__)
@register.inclusion_tag('allianceauth/admin-status/overview.html', takes_context=True)
def status_overview(context):
@register.inclusion_tag('allianceauth/admin-status/overview.html')
def status_overview() -> dict:
response = {
'notifications': list(),
'current_version': __version__,
@@ -46,7 +46,7 @@ def status_overview(context):
return response
def _fetch_celery_queue_length():
def _fetch_celery_queue_length() -> int:
try:
app = app_or_default(None)
with app.connection_or_acquire() as conn:
@@ -69,11 +69,15 @@ def _current_notifications() -> dict:
'gitlab_notification_issues',
_fetch_notification_issues_from_gitlab,
NOTIFICATION_CACHE_TIME
)
top_notifications = notifications[:5]
)
except requests.RequestException:
logger.exception('Error while getting gitlab notifications')
top_notifications = []
else:
if notifications:
top_notifications = notifications[:5]
else:
top_notifications = []
response = {
'notifications': top_notifications,
@@ -95,8 +99,15 @@ def _current_version_summary() -> dict:
logger.exception('Error while getting gitlab release tags')
return {}
latest_major_version, latest_minor_version, latest_patch_version, latest_beta_version = \
_latests_versions(tags)
if not tags:
return {}
(
latest_major_version,
latest_minor_version,
latest_patch_version,
latest_beta_version
) = _latests_versions(tags)
current_version = Pep440Version(__version__)
has_latest_major = \
@@ -107,8 +118,8 @@ def _current_version_summary() -> dict:
current_version >= latest_patch_version if latest_patch_version else False
has_current_beta = \
current_version.base_version <= latest_beta_version.base_version \
and latest_major_version.base_version <= latest_beta_version.base_version \
if latest_beta_version else False
and latest_major_version.base_version <= latest_beta_version.base_version \
if latest_beta_version else False
response = {
'latest_major': has_latest_major,
@@ -146,7 +157,6 @@ def _latests_versions(tags: list) -> tuple:
else:
versions.append(version)
latest_version = latest_patch_version = max(versions)
latest_major_version = min([
v for v in versions if v.major == latest_version.major
@@ -156,10 +166,15 @@ def _latests_versions(tags: list) -> tuple:
if v.major == latest_version.major and v.minor == latest_version.minor
])
latest_beta_version = max(betas)
return latest_major_version, latest_minor_version, latest_patch_version, latest_beta_version
return (
latest_major_version,
latest_minor_version,
latest_patch_version,
latest_beta_version
)
def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES):
def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list:
"""returns a list from the GitLab API. Supports pageing"""
result = list()
for page in range(1, max_pages + 1):

BIN
allianceauth_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -0,0 +1,45 @@
# Contributing
Alliance Auth is developed by the community and we are always looking to welcome new contributors. If you are interested in contributing, here are some ideas where to start:
## Publish a new community app or service
One great way to contribute is to develop and publish your own community app or service for Alliance Auth. By design Auth only comes with some basic features and therefore heavily relies on the community to provide apps to extend Auth with additional features.
To publish your app make sure it can be installed from a public repo or PyPI. Once it's ready, you can inform everybody about your new app by posting it to our [list of community apps](/features/community/index.md).
If you are looking for ideas on what to make, you can check out Auth's [issue list](https://gitlab.com/allianceauth/allianceauth/-/issues?scope=all&utf8=%E2%9C%93&state=opened&label_name[]=enhancement). Many of those issues are feature requests that will probably never make into Auth core, but would be awesome to have as community app or service. You could also ask the other devs on our Discord server for ideas or to help you get a feeling about which new features might be in higher demand than others.
## Help to maintain an existing community app or service
There are quite a few great community apps that need help from additional maintainers. Often the initial author has no time anymore to support his app or would just appreciate some support for working on new features or to fix bugs.
Sometimes original app owners may even be looking to completely hand over their apps to a new owner.
If you are interested to help maintain an existing community app or service you can just start working on open issues and create merge requests. Or just ask other devs on our Discord.
## Help with improving Auth documentation
Auth has an extensive [documentation](https://allianceauth.readthedocs.io/en/latest/), but there are always things to improve and add. If you notice any errors or see something to improve or add please feel free to issue a change for the documentation (via MRs same as code changes).
## Help with support questions on Discord
One of the main functions of the Auth Discord server is to help the community with any support question they may have when installing or running an Auth installation.
Note that you do not need a be part of any official group to become a supporter. Just jump in and help with answering new questions from the community if you know how to help.
## Help to improve Alliance Auth core
Alliance Auth has an issue list, which is usually the basis for all maintenance activities for Auth core. That means that bug fixes and new features are primarily delivered based on existing open issues.
We usually have a long list of open issues and very much welcome every help to fix existing bugs or work on new features for Auth.
Before starting to code on any topic we'd suggest talking to the other devs on Discord to make sure your issue is not already being worked on. Also, some feature request may be better implemented in a community app. Another aspect, which is best clarified by talking with the other devs.
If you like to contribute to Auth core, but are unsure where to start, we have a dedicated label for issues that are suitable for beginners: [beginner friendly](https://gitlab.com/allianceauth/allianceauth/-/issues?label_name%5B%5D=beginner+friendly).
## Additional Resources
For more information on how to create community apps or how to setup a developer environment for Auth, please see our official [developer documentation](/development/index.md).
For getting in touch with other contributors please feel free to join the [Alliance Auth Discord server](https://discord.gg/fjnHAmk).

View File

@@ -4,4 +4,4 @@ Another key feature of **Alliance Auth** is that it can be easily extended. Our
Check out the [Community Creations](https://gitlab.com/allianceauth/community-creations) repo for more details.
Or if you have very specific needs you can of course develop your own plugin- apps and services. Please see the [Development](/development/index.md) chapter details.
Or if you have specific needs you can of course always develop your own plugin- apps and services. Please see the [Development](/development/index.md) chapter for details.

View File

@@ -131,8 +131,8 @@ Name Description
`DISCORD_BOT_TOKEN` Generated bot token for the Discord Auth app `''`
`DISCORD_CALLBACK_URL` Oauth callback URL `''`
`DISCORD_GUILD_ID` Discord ID of your Discord server `''`
`DISCORD_GUILD_NAME_CACHE_MAX_AGE` How long the Discord server name is cached locally in milliseconds `3600000`
`DISCORD_ROLES_CACHE_MAX_AGE` How long roles retrieved from the Discord server are cached locally in milliseconds `3600000`
`DISCORD_GUILD_NAME_CACHE_MAX_AGE` How long the Discord server name is cached locally in seconds `86400`
`DISCORD_ROLES_CACHE_MAX_AGE` How long roles retrieved from the Discord server are cached locally in seconds `3600`
`DISCORD_SYNC_NAMES` When set to True the nicknames of Discord users will be set to the user's main character name `False`
`DISCORD_TASKS_RETRY_PAUSE` Pause in seconds until next retry for tasks after an error occurred `60`
`DISCORD_TASKS_MAX_RETRIES` max retries of tasks after an error occurred `3`
@@ -144,3 +144,11 @@ Name Description
### "Unknown Error" on Discord site when activating service
This indicates your callback URL doesn't match. Ensure the `DISCORD_CALLBACK_URL` setting exactly matches the URL entered on the Discord developers site. This includes http(s), trailing slash, etc.
### "Add/Remove" Errors in Discord Service
If you are recieving errors in your Notifications after verifying that your settings are all correct try the following:
- Ensure that the bot's role in Discord is at the top of the roles list. Each time you add it to your server you will need to do this again.
- Make sure that the bot is not trying to modify the Owner of the discord, as it will fail. A holding discord account added with invite link will mitigate this.
- Make sure that the bot role on discord has all needed permissions, Admin etc., remembering that these will need to be set every time you add the bot to the Discord server.

View File

@@ -215,3 +215,29 @@ On a freshly installed mumble server only your superuser has the right to config
- user: `SuperUser`
- password: *what you defined when configuring your mumble server*
## General notes
### Setting a server password
With the default configuration your mumble server is public. Meaning that everyone who has the address can at least connect to it and might also be able join all channels that don't have any permissions set (Depending on your ACL configured for the root channel). If you want only registered member being able to join your mumble, you have to set a server password. To do so open your mumble server configuration which is by default located at `/etc/mumble-server.ini`.
```bash
nano /etc/mumble-server.ini
```
Now search for `serverpassword=` and set your password here. If there is no such line, simply add it.
```text
serverpassword=YourSuperSecretServerPassword
```
Save the file and restart your mumble server afterwards.
```bash
service mumble-server restart
```
From now on, only registerd member can join your mumble server. Now if you still want to allow guests to join you have 2 options.
- Allow the "Guest" state to activate the Mumble service in your Auth instance
- Use [Mumble temporary links](https://github.com/pvyParts/allianceauth-mumble-temp)

View File

@@ -101,7 +101,7 @@ The method which populates these runs every 30 minutes. To populate manually, st
And execute the update:
from services.modules.teamspeak3.tasks import Teamspeak3Tasks
from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks
Teamspeak3Tasks.run_ts3_group_update()
Ensure that command does not return an error.

View File

@@ -18,4 +18,5 @@ Welcome to the official documentation for **Alliance Auth**!
support/index
customizing/index
development/index
contributing/index
```

View File

@@ -32,7 +32,7 @@ install_requires = [
'openfire-restapi',
'sleekxmpp',
'django-esi>=1.5.0,<2.0'
'django-esi>=1.5,<3.0'
]
testing_extras = [