mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 06:06:19 +01:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3874aa6fee | ||
|
|
103e9f3a11 | ||
|
|
d02c25f421 | ||
|
|
228af38a4a | ||
|
|
051a48885c | ||
|
|
6bcdc6052f | ||
|
|
af3527e64f | ||
|
|
17ef3dd07a | ||
|
|
1f165ecd2a | ||
|
|
70d1d450a9 | ||
|
|
b667892698 | ||
|
|
dc11add0e9 | ||
|
|
cb429a0b88 | ||
|
|
b51039cfc0 | ||
|
|
eadd959d95 | ||
|
|
1856e03d88 | ||
|
|
7dcfa622a3 | ||
|
|
64251b9b3c | ||
|
|
6b073dd5fc | ||
|
|
0911fabfb2 | ||
|
|
050d3f5e63 | ||
|
|
bbe3f78ad1 | ||
|
|
8204c18895 | ||
|
|
b91c788897 | ||
|
|
1d20a3029f | ||
|
|
9cfebc9ae3 | ||
|
|
ddabb4539b | ||
|
|
ada35e221b | ||
|
|
6fef9d904e | ||
|
|
67cf2b5904 | ||
|
|
3bebe792f6 | ||
|
|
00b4d89181 | ||
|
|
f729c6b650 | ||
|
|
df95f8c3f3 | ||
|
|
fe36e57d72 | ||
|
|
31197812b6 | ||
|
|
bd3fe01a12 | ||
|
|
39f7f32b7d | ||
|
|
b4522a1277 | ||
|
|
bb6a7e8327 | ||
|
|
9bd42a7579 | ||
|
|
b41430e5a3 | ||
|
|
595353e838 | ||
|
|
f1a21bb856 | ||
|
|
e44c2935f9 | ||
|
|
4d546f948d | ||
|
|
3bab349d7b | ||
|
|
eef6126ef8 | ||
|
|
5c7478fa39 | ||
|
|
64b72d0b06 | ||
|
|
b266a98b25 | ||
|
|
8a27de5df8 | ||
|
|
f9b5310fce | ||
|
|
fdce173969 | ||
|
|
7b9ddf90c1 | ||
|
|
580c8c19de | ||
|
|
55cc77140e | ||
|
|
93c89dd7cc | ||
|
|
c970cbbd2d | ||
|
|
9ea55fa51f | ||
|
|
5775a11b4e | ||
|
|
1a666b6584 | ||
|
|
35407a2108 | ||
|
|
71fb19aa22 | ||
|
|
b7d7f7b8ce | ||
|
|
59b983edcc | ||
|
|
1734d034e1 | ||
|
|
7f7500ff0c | ||
|
|
ce77c24e5c | ||
|
|
5469a591c0 | ||
|
|
a4befc5e59 | ||
|
|
1ee8065592 | ||
|
|
e4e3bd44fc | ||
|
|
c75de07c2e | ||
|
|
e928131809 | ||
|
|
4f802e82a9 | ||
|
|
0c90bd462e | ||
|
|
bbb70c93d9 | ||
|
|
f6e6ba775c | ||
|
|
06646be907 | ||
|
|
1b4c1a4b9e | ||
|
|
ae3f5a0f62 | ||
|
|
3a984e8a4d | ||
|
|
7d711a54bc | ||
|
|
d92d629c25 | ||
|
|
21e630209a | ||
|
|
e3933998ef | ||
|
|
667afe9051 | ||
|
|
26dc2881eb | ||
|
|
250cb33285 | ||
|
|
db51abec1f | ||
|
|
530716d458 | ||
|
|
f3065d79b3 | ||
|
|
bca5f0472e | ||
|
|
8e54c43917 | ||
|
|
946df1d7a0 | ||
|
|
55f00f742c | ||
|
|
18584974df | ||
|
|
6c275d4cd2 | ||
|
|
2d64ee5e2a | ||
|
|
57d9ddc2c6 |
@@ -36,7 +36,7 @@ Main features:
|
||||
|
||||
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||
|
||||
- Chinese :cn:, English :us:, German :de: and Spanish :es: localization
|
||||
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
|
||||
|
||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
||||
|
||||
|
||||
@@ -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.6.6a10'
|
||||
__version__ = '2.8.0a1'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = '%s v%s' % (__title__, __version__)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -69,33 +69,33 @@ class TestCaseWithTestData(TestCase):
|
||||
|
||||
# user 1 - corp and alliance, normal user
|
||||
character_1 = EveCharacter.objects.create(
|
||||
character_id='1001',
|
||||
character_id=1001,
|
||||
character_name='Bruce Wayne',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
character_1a = EveCharacter.objects.create(
|
||||
character_id='1002',
|
||||
character_id=1002,
|
||||
character_name='Batman',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
executor_corp_id='2001'
|
||||
executor_corp_id=2001
|
||||
)
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
member_count=42,
|
||||
@@ -169,10 +169,10 @@ class TestCaseWithTestData(TestCase):
|
||||
alliance=None
|
||||
)
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id='3101',
|
||||
alliance_id=3101,
|
||||
alliance_name='Lex World Domination',
|
||||
alliance_ticker='LWD',
|
||||
executor_corp_id=''
|
||||
executor_corp_id=2101
|
||||
)
|
||||
cls.user_3 = User.objects.create_user(
|
||||
character_3.character_name.replace(' ', '_'),
|
||||
@@ -510,8 +510,8 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
('2002', 'Daily Planet'),
|
||||
('2001', 'Wayne Technologies'),
|
||||
(2002, 'Daily Planet'),
|
||||
(2001, 'Wayne Technologies'),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
@@ -540,7 +540,7 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
('3001', 'Wayne Enterprises'),
|
||||
(3001, 'Wayne Enterprises'),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
|
||||
149
allianceauth/authentication/tests/test_backend.py
Normal file
149
allianceauth/authentication/tests/test_backend.py
Normal 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'))
|
||||
35
allianceauth/authentication/tests/test_commands.py
Normal file
35
allianceauth/authentication/tests/test_commands.py
Normal 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())
|
||||
68
allianceauth/authentication/tests/test_decorators.py
Normal file
68
allianceauth/authentication/tests/test_decorators.py
Normal 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)
|
||||
@@ -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):
|
||||
@@ -343,10 +216,10 @@ class CharacterOwnershipCheckTestCase(TestCase):
|
||||
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
||||
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
|
||||
corp_name='Test Corp', alliance_name='Test Alliance')
|
||||
cls.character = EveCharacter.objects.get(character_id='1')
|
||||
cls.character = EveCharacter.objects.get(character_id=1)
|
||||
cls.token = Token.objects.create(
|
||||
user=cls.user,
|
||||
character_id='1',
|
||||
character_id=1,
|
||||
character_name='Test',
|
||||
character_owner_hash='1',
|
||||
)
|
||||
@@ -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())
|
||||
290
allianceauth/authentication/tests/test_templatetags.py
Normal file
290
allianceauth/authentication/tests/test_templatetags.py
Normal file
@@ -0,0 +1,290 @@
|
||||
from math import ceil
|
||||
from unittest.mock import patch
|
||||
|
||||
from requests import RequestException
|
||||
import requests_mock
|
||||
from packaging.version import Version as Pep440Version
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.templatetags.admin_status import (
|
||||
status_overview,
|
||||
_fetch_list_from_gitlab,
|
||||
_current_notifications,
|
||||
_current_version_summary,
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
_fetch_tags_from_gitlab,
|
||||
_latests_versions
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.templatetags'
|
||||
|
||||
|
||||
def create_tags_list(tag_names: list):
|
||||
return [{'name': str(tag_name)} for tag_name in tag_names]
|
||||
|
||||
|
||||
GITHUB_TAGS = create_tags_list(['v2.4.6a1', 'v2.4.5', 'v2.4.0', 'v2.0.0', 'v1.1.1'])
|
||||
GITHUB_NOTIFICATION_ISSUES = [
|
||||
{
|
||||
'id': 1,
|
||||
'title': 'first issue'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'title': 'second issue'
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'title': 'third issue'
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'title': 'forth issue'
|
||||
},
|
||||
{
|
||||
'id': 5,
|
||||
'title': 'fifth issue'
|
||||
},
|
||||
{
|
||||
'id': 6,
|
||||
'title': 'sixth issue'
|
||||
},
|
||||
]
|
||||
TEST_VERSION = '2.6.5'
|
||||
|
||||
|
||||
class TestStatusOverviewTag(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
||||
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
||||
def test_status_overview(
|
||||
self,
|
||||
mock_current_notifications,
|
||||
mock_current_version_info,
|
||||
mock_fetch_celery_queue_length
|
||||
):
|
||||
notifications = {
|
||||
'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
|
||||
}
|
||||
mock_current_notifications.return_value = notifications
|
||||
version_info = {
|
||||
'latest_major': True,
|
||||
'latest_minor': True,
|
||||
'latest_patch': True,
|
||||
'latest_beta': False,
|
||||
'current_version': TEST_VERSION,
|
||||
'latest_major_version': '2.4.5',
|
||||
'latest_minor_version': '2.4.0',
|
||||
'latest_patch_version': '2.4.5',
|
||||
'latest_beta_version': '2.4.4a1',
|
||||
}
|
||||
mock_current_version_info.return_value = version_info
|
||||
mock_fetch_celery_queue_length.return_value = 3
|
||||
|
||||
result = status_overview()
|
||||
expected = {
|
||||
'notifications': GITHUB_NOTIFICATION_ISSUES[:5],
|
||||
'latest_major': True,
|
||||
'latest_minor': True,
|
||||
'latest_patch': True,
|
||||
'latest_beta': False,
|
||||
'current_version': TEST_VERSION,
|
||||
'latest_major_version': '2.4.5',
|
||||
'latest_minor_version': '2.4.0',
|
||||
'latest_patch_version': '2.4.5',
|
||||
'latest_beta_version': '2.4.4a1',
|
||||
'task_queue_length': 3,
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
|
||||
'?labels=announcement'
|
||||
)
|
||||
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
|
||||
result = _fetch_notification_issues_from_gitlab()
|
||||
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_notifications_normal(self, mock_cache):
|
||||
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
|
||||
|
||||
result = _current_notifications()
|
||||
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_notifications_failed(self, mock_cache):
|
||||
mock_cache.get_or_set.side_effect = RequestException
|
||||
|
||||
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):
|
||||
|
||||
def test_get_celery_queue_length(self):
|
||||
pass
|
||||
|
||||
|
||||
class TestVersionTags(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_version_info_normal(self, mock_cache):
|
||||
mock_cache.get_or_set.return_value = GITHUB_TAGS
|
||||
|
||||
result = _current_version_summary()
|
||||
self.assertTrue(result['latest_major'])
|
||||
self.assertTrue(result['latest_minor'])
|
||||
self.assertTrue(result['latest_patch'])
|
||||
self.assertEqual(result['latest_major_version'], '2.0.0')
|
||||
self.assertEqual(result['latest_minor_version'], '2.4.0')
|
||||
self.assertEqual(result['latest_patch_version'], '2.4.5')
|
||||
self.assertEqual(result['latest_beta_version'], '2.4.6a1')
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_version_info_failed(self, mock_cache):
|
||||
mock_cache.get_or_set.side_effect = RequestException
|
||||
|
||||
expected = {}
|
||||
result = _current_version_summary()
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_fetch_tags_from_gitlab(self, requests_mocker):
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||
'/repository/tags'
|
||||
)
|
||||
requests_mocker.get(url, json=GITHUB_TAGS)
|
||||
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):
|
||||
|
||||
def test_all_version_types_defined(self):
|
||||
|
||||
tags = create_tags_list(
|
||||
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
|
||||
)
|
||||
major, minor, patch, beta = _latests_versions(tags)
|
||||
self.assertEqual(major, Pep440Version('2.0.0'))
|
||||
self.assertEqual(minor, Pep440Version('2.1.0'))
|
||||
self.assertEqual(patch, Pep440Version('2.1.1'))
|
||||
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||
|
||||
def test_major_and_minor_not_defined_with_zero(self):
|
||||
|
||||
tags = create_tags_list(
|
||||
['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
|
||||
)
|
||||
major, minor, patch, beta = _latests_versions(tags)
|
||||
self.assertEqual(major, Pep440Version('2.0.1'))
|
||||
self.assertEqual(minor, Pep440Version('2.1.1'))
|
||||
self.assertEqual(patch, Pep440Version('2.1.2'))
|
||||
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||
|
||||
def test_can_ignore_invalid_versions(self):
|
||||
|
||||
tags = create_tags_list(
|
||||
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid']
|
||||
)
|
||||
major, minor, patch, beta = _latests_versions(tags)
|
||||
self.assertEqual(major, Pep440Version('2.0.0'))
|
||||
self.assertEqual(minor, Pep440Version('2.1.0'))
|
||||
self.assertEqual(patch, Pep440Version('2.1.1'))
|
||||
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||
|
||||
|
||||
class TestFetchListFromGitlab(TestCase):
|
||||
|
||||
page_size = 2
|
||||
|
||||
def setUp(self):
|
||||
self.url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||
'/repository/tags'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def my_callback(cls, request, context):
|
||||
page = int(request.qs['page'][0])
|
||||
start = (page - 1) * cls.page_size
|
||||
end = start + cls.page_size
|
||||
return GITHUB_TAGS[start:end]
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_can_fetch_one_page_with_header(self, requests_mocker):
|
||||
headers = {
|
||||
'x-total-pages': '1'
|
||||
}
|
||||
requests_mocker.get(self.url, json=GITHUB_TAGS, headers=headers)
|
||||
result = _fetch_list_from_gitlab(self.url)
|
||||
self.assertEqual(result, GITHUB_TAGS)
|
||||
self.assertEqual(requests_mocker.call_count, 1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_can_fetch_one_page_wo_header(self, requests_mocker):
|
||||
requests_mocker.get(self.url, json=GITHUB_TAGS)
|
||||
result = _fetch_list_from_gitlab(self.url)
|
||||
self.assertEqual(result, GITHUB_TAGS)
|
||||
self.assertEqual(requests_mocker.call_count, 1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_can_fetch_one_page_and_ignore_invalid_header(self, requests_mocker):
|
||||
headers = {
|
||||
'x-total-pages': 'invalid'
|
||||
}
|
||||
requests_mocker.get(self.url, json=GITHUB_TAGS, headers=headers)
|
||||
result = _fetch_list_from_gitlab(self.url)
|
||||
self.assertEqual(result, GITHUB_TAGS)
|
||||
self.assertEqual(requests_mocker.call_count, 1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_can_fetch_multiple_pages(self, requests_mocker):
|
||||
total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
|
||||
headers = {
|
||||
'x-total-pages': str(total_pages)
|
||||
}
|
||||
requests_mocker.get(self.url, json=self.my_callback, headers=headers)
|
||||
result = _fetch_list_from_gitlab(self.url)
|
||||
self.assertEqual(result, GITHUB_TAGS)
|
||||
self.assertEqual(requests_mocker.call_count, total_pages)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_can_fetch_given_number_of_pages_only(self, requests_mocker):
|
||||
total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
|
||||
headers = {
|
||||
'x-total-pages': str(total_pages)
|
||||
}
|
||||
requests_mocker.get(self.url, json=self.my_callback, headers=headers)
|
||||
max_pages = 2
|
||||
result = _fetch_list_from_gitlab(self.url, max_pages=max_pages)
|
||||
self.assertEqual(result, GITHUB_TAGS[:4])
|
||||
self.assertEqual(requests_mocker.call_count, max_pages)
|
||||
@@ -8,7 +8,7 @@ class CorpStats(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
_('Corporation Stats'),
|
||||
'fa fa-share-alt fa-fw',
|
||||
'fas fa-share-alt fa-fw',
|
||||
'corputils:view',
|
||||
navactive=['corputils:'])
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ class CorpStatsManagerTestCase(TestCase):
|
||||
cls.user = AuthUtils.create_user('test')
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
|
||||
cls.user.profile.refresh_from_db()
|
||||
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
|
||||
cls.alliance = EveAllianceInfo.objects.create(alliance_id=3, alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id=2)
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id=2, corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id=1, character_name='test character', character_owner_hash='z')
|
||||
cls.corpstats = CorpStats.objects.create(corp=cls.corp, token=cls.token)
|
||||
cls.view_corp_permission = Permission.objects.get_by_natural_key('view_corp_corpstats', 'corputils', 'corpstats')
|
||||
cls.view_alliance_permission = Permission.objects.get_by_natural_key('view_alliance_corpstats', 'corputils', 'corpstats')
|
||||
@@ -66,9 +66,9 @@ class CorpStatsUpdateTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('test')
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id=2, corp_name='test_corp', corp_ticker='TEST', alliance_id=3, alliance_name='TEST')
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id=1, character_name='test character', character_owner_hash='z')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id=2, corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
|
||||
def setUp(self):
|
||||
self.corpstats = CorpStats.objects.get_or_create(token=self.token, corp=self.corp)[0]
|
||||
@@ -88,11 +88,11 @@ class CorpStatsUpdateTestCase(TestCase):
|
||||
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
|
||||
SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
|
||||
self.corpstats.update()
|
||||
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
|
||||
self.assertTrue(CorpMember.objects.filter(character_id=1, character_name='test character', corpstats=self.corpstats).exists())
|
||||
|
||||
@mock.patch('esi.clients.SwaggerClient')
|
||||
def test_update_remove_member(self, SwaggerClient):
|
||||
CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats)
|
||||
CorpMember.objects.create(character_id=2, character_name='old test character', corpstats=self.corpstats)
|
||||
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
|
||||
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
|
||||
SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
|
||||
@@ -130,15 +130,15 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = AuthUtils.create_user('test')
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id=2, corp_name='test_corp', corp_ticker='TEST', alliance_id=3, alliance_name='TEST')
|
||||
cls.user.profile.refresh_from_db()
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id=1, character_name='test character', character_owner_hash='z')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id=2, corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
|
||||
cls.character = EveCharacter.objects.create(character_name='another test character', character_id='4', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
|
||||
cls.character = EveCharacter.objects.create(character_name='another test character', character_id=4, corporation_id=2, corporation_name='test corp', corporation_ticker='TEST')
|
||||
|
||||
def test_member_count(self):
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id=2, character_name='test character')
|
||||
self.assertEqual(self.corpstats.member_count, 1)
|
||||
member.delete()
|
||||
self.assertEqual(self.corpstats.member_count, 0)
|
||||
@@ -147,7 +147,7 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
AuthUtils.disconnect_signals()
|
||||
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
|
||||
AuthUtils.connect_signals()
|
||||
CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
|
||||
CorpMember.objects.create(corpstats=self.corpstats, character_id=4, character_name='test character')
|
||||
self.assertEqual(self.corpstats.user_count, 1)
|
||||
co.delete()
|
||||
self.assertEqual(self.corpstats.user_count, 0)
|
||||
@@ -156,7 +156,8 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
AuthUtils.disconnect_signals()
|
||||
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
|
||||
AuthUtils.connect_signals()
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id=4, character_name='test character')
|
||||
self.corpstats.refresh_from_db()
|
||||
self.assertIn(member, self.corpstats.registered_members)
|
||||
self.assertEqual(self.corpstats.registered_member_count, 1)
|
||||
|
||||
@@ -165,7 +166,7 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
self.assertEqual(self.corpstats.registered_member_count, 0)
|
||||
|
||||
def test_unregistered_members(self):
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id=4, character_name='test character')
|
||||
self.corpstats.refresh_from_db()
|
||||
self.assertIn(member, self.corpstats.unregistered_members)
|
||||
self.assertEqual(self.corpstats.unregistered_member_count, 1)
|
||||
@@ -178,13 +179,13 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
|
||||
def test_mains(self):
|
||||
# test when is a main
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
|
||||
member = CorpMember.objects.create(corpstats=self.corpstats, character_id=1, character_name='test character')
|
||||
self.assertIn(member, self.corpstats.mains)
|
||||
self.assertEqual(self.corpstats.main_count, 1)
|
||||
|
||||
# test when is an alt
|
||||
old_main = self.user.profile.main_character
|
||||
character = EveCharacter.objects.create(character_name='other character', character_id=10, corporation_name='test corp', corporation_id='2', corporation_ticker='TEST')
|
||||
character = EveCharacter.objects.create(character_name='other character', character_id=10, corporation_name='test corp', corporation_id=2, corporation_ticker='TEST')
|
||||
AuthUtils.disconnect_signals()
|
||||
co = CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
|
||||
self.user.profile.main_character = character
|
||||
@@ -208,7 +209,7 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
self.assertEqual(self.corpstats.corp_logo(size=128), 'https://images.evetech.net/corporations/2/logo?size=128')
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://images.evetech.net/alliances/1/logo?size=128')
|
||||
|
||||
alliance = EveAllianceInfo.objects.create(alliance_name='test alliance', alliance_id='3', alliance_ticker='TEST', executor_corp_id='2')
|
||||
alliance = EveAllianceInfo.objects.create(alliance_name='test alliance', alliance_id=3, alliance_ticker='TEST', executor_corp_id=2)
|
||||
self.corp.alliance = alliance
|
||||
self.corp.save()
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://images.evetech.net/alliances/3/logo?size=128')
|
||||
@@ -221,14 +222,14 @@ class CorpMemberTestCase(TestCase):
|
||||
cls.user = AuthUtils.create_user('test')
|
||||
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
|
||||
cls.user.profile.refresh_from_db()
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='a')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id=1, character_name='test character', character_owner_hash='a')
|
||||
cls.corp = EveCorporationInfo.objects.create(corporation_id=2, corporation_name='test corp', corporation_ticker='TEST', member_count=1)
|
||||
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
|
||||
cls.member = CorpMember.objects.create(corpstats=cls.corpstats, character_id='2', character_name='other test character')
|
||||
cls.member = CorpMember.objects.create(corpstats=cls.corpstats, character_id=2, character_name='other test character')
|
||||
|
||||
def test_character(self):
|
||||
self.assertIsNone(self.member.character)
|
||||
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
|
||||
character = EveCharacter.objects.create(character_id=2, character_name='other test character', corporation_id=2, corporation_name='test corp', corporation_ticker='TEST')
|
||||
self.assertEqual(self.member.character, character)
|
||||
|
||||
def test_main_character(self):
|
||||
@@ -238,7 +239,7 @@ class CorpMemberTestCase(TestCase):
|
||||
self.assertIsNone(self.member.main_character)
|
||||
|
||||
# test when member.character is not None but also not a main
|
||||
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
|
||||
character = EveCharacter.objects.create(character_id=2, character_name='other test character', corporation_id=2, corporation_name='test corp', corporation_ticker='TEST')
|
||||
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
|
||||
self.member.refresh_from_db()
|
||||
self.assertNotEqual(self.member.main_character, self.member.character)
|
||||
@@ -260,14 +261,14 @@ class CorpMemberTestCase(TestCase):
|
||||
def test_alts(self):
|
||||
self.assertListEqual(self.member.alts, [])
|
||||
|
||||
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
|
||||
character = EveCharacter.objects.create(character_id=2, character_name='other test character', corporation_id=2, corporation_name='test corp', corporation_ticker='TEST')
|
||||
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
|
||||
self.assertIn(character, self.member.alts)
|
||||
|
||||
def test_registered(self):
|
||||
self.assertFalse(self.member.registered)
|
||||
AuthUtils.disconnect_signals()
|
||||
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
|
||||
character = EveCharacter.objects.create(character_id=2, character_name='other test character', corporation_id=2, corporation_name='test corp', corporation_ticker='TEST')
|
||||
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
|
||||
self.assertTrue(self.member.registered)
|
||||
AuthUtils.connect_signals()
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
# It contains of modules for views and templatetags for templates
|
||||
|
||||
# list of all eve entity categories as defined in ESI
|
||||
ESI_CATEGORY_AGENT = "agent"
|
||||
ESI_CATEGORY_ALLIANCE = "alliance"
|
||||
ESI_CATEGORY_CHARACTER = "character"
|
||||
ESI_CATEGORY_CONSTELLATION = "constellation"
|
||||
ESI_CATEGORY_CORPORATION = "corporation"
|
||||
ESI_CATEGORY_FACTION = "faction"
|
||||
ESI_CATEGORY_INVENTORYTYPE = "inventory_type"
|
||||
ESI_CATEGORY_REGION = "region"
|
||||
ESI_CATEGORY_SOLARSYSTEM = "solar_system"
|
||||
ESI_CATEGORY_STATION = "station"
|
||||
ESI_CATEGORY_WORMHOLE = "wormhole"
|
||||
_ESI_CATEGORY_AGENT = "agent"
|
||||
_ESI_CATEGORY_ALLIANCE = "alliance"
|
||||
_ESI_CATEGORY_CHARACTER = "character"
|
||||
_ESI_CATEGORY_CONSTELLATION = "constellation"
|
||||
_ESI_CATEGORY_CORPORATION = "corporation"
|
||||
_ESI_CATEGORY_FACTION = "faction"
|
||||
_ESI_CATEGORY_INVENTORYTYPE = "inventory_type"
|
||||
_ESI_CATEGORY_REGION = "region"
|
||||
_ESI_CATEGORY_SOLARSYSTEM = "solar_system"
|
||||
_ESI_CATEGORY_STATION = "station"
|
||||
_ESI_CATEGORY_WORMHOLE = "wormhole"
|
||||
|
||||
@@ -2,24 +2,30 @@
|
||||
|
||||
from urllib.parse import urljoin, quote
|
||||
|
||||
from . import *
|
||||
from . import (
|
||||
_ESI_CATEGORY_ALLIANCE,
|
||||
_ESI_CATEGORY_CORPORATION,
|
||||
_ESI_CATEGORY_REGION,
|
||||
_ESI_CATEGORY_SOLARSYSTEM
|
||||
)
|
||||
|
||||
BASE_URL = 'http://evemaps.dotlan.net'
|
||||
|
||||
_BASE_URL = 'http://evemaps.dotlan.net'
|
||||
|
||||
|
||||
def _build_url(category: str, name: str) -> str:
|
||||
"""return url to profile page for an eve entity"""
|
||||
|
||||
if category == ESI_CATEGORY_ALLIANCE:
|
||||
if category == _ESI_CATEGORY_ALLIANCE:
|
||||
partial = 'alliance'
|
||||
|
||||
elif category == ESI_CATEGORY_CORPORATION:
|
||||
elif category == _ESI_CATEGORY_CORPORATION:
|
||||
partial = 'corp'
|
||||
|
||||
elif category == ESI_CATEGORY_REGION:
|
||||
elif category == _ESI_CATEGORY_REGION:
|
||||
partial = 'map'
|
||||
|
||||
elif category == ESI_CATEGORY_SOLARSYSTEM:
|
||||
elif category == _ESI_CATEGORY_SOLARSYSTEM:
|
||||
partial = 'system'
|
||||
|
||||
else:
|
||||
@@ -28,7 +34,7 @@ def _build_url(category: str, name: str) -> str:
|
||||
)
|
||||
|
||||
url = urljoin(
|
||||
BASE_URL,
|
||||
_BASE_URL,
|
||||
'{}/{}'.format(partial, quote(str(name).replace(" ", "_")))
|
||||
|
||||
)
|
||||
@@ -37,16 +43,19 @@ def _build_url(category: str, name: str) -> str:
|
||||
|
||||
def alliance_url(name: str) -> str:
|
||||
"""url for page about given alliance on dotlan"""
|
||||
return _build_url(ESI_CATEGORY_ALLIANCE, name)
|
||||
return _build_url(_ESI_CATEGORY_ALLIANCE, name)
|
||||
|
||||
|
||||
def corporation_url(name: str) -> str:
|
||||
"""url for page about given corporation on dotlan"""
|
||||
return _build_url(ESI_CATEGORY_CORPORATION, name)
|
||||
return _build_url(_ESI_CATEGORY_CORPORATION, name)
|
||||
|
||||
|
||||
def region_url(name: str) -> str:
|
||||
"""url for page about given region on dotlan"""
|
||||
return _build_url(ESI_CATEGORY_REGION, name)
|
||||
return _build_url(_ESI_CATEGORY_REGION, name)
|
||||
|
||||
|
||||
def solar_system_url(name: str) -> str:
|
||||
"""url for page about given solar system on dotlan"""
|
||||
return _build_url(ESI_CATEGORY_SOLARSYSTEM, name)
|
||||
return _build_url(_ESI_CATEGORY_SOLARSYSTEM, name)
|
||||
|
||||
129
allianceauth/eveonline/evelinks/eveimageserver.py
Normal file
129
allianceauth/eveonline/evelinks/eveimageserver.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from . import (
|
||||
_ESI_CATEGORY_ALLIANCE,
|
||||
_ESI_CATEGORY_CHARACTER,
|
||||
_ESI_CATEGORY_CORPORATION,
|
||||
_ESI_CATEGORY_INVENTORYTYPE
|
||||
)
|
||||
|
||||
|
||||
_EVE_IMAGE_SERVER_URL = 'https://images.evetech.net'
|
||||
_DEFAULT_IMAGE_SIZE = 32
|
||||
|
||||
|
||||
def _eve_entity_image_url(
|
||||
category: str,
|
||||
entity_id: int,
|
||||
size: int = 32,
|
||||
variant: str = None,
|
||||
tenant: str = None,
|
||||
) -> str:
|
||||
"""returns image URL for an Eve Online ID.
|
||||
Supported categories: alliance, corporation, character, inventory_type
|
||||
|
||||
Arguments:
|
||||
- category: category of the ID, see ESI category constants
|
||||
- entity_id: Eve ID of the entity
|
||||
- size: (optional) render size of the image.must be between 32 (default) and 1024
|
||||
- variant: (optional) image variant for category. currently not relevant.
|
||||
- tenant: (optional) Eve Server, either `tranquility`(default) or `singularity`
|
||||
|
||||
Returns:
|
||||
- URL string for the requested image on the Eve image server
|
||||
|
||||
Exceptions:
|
||||
- Throws ValueError on invalid input
|
||||
"""
|
||||
|
||||
# input validations
|
||||
categories = {
|
||||
_ESI_CATEGORY_ALLIANCE: {
|
||||
'endpoint': 'alliances',
|
||||
'variants': ['logo']
|
||||
},
|
||||
_ESI_CATEGORY_CORPORATION: {
|
||||
'endpoint': 'corporations',
|
||||
'variants': ['logo']
|
||||
},
|
||||
_ESI_CATEGORY_CHARACTER: {
|
||||
'endpoint': 'characters',
|
||||
'variants': ['portrait']
|
||||
},
|
||||
_ESI_CATEGORY_INVENTORYTYPE: {
|
||||
'endpoint': 'types',
|
||||
'variants': ['icon', 'render']
|
||||
}
|
||||
}
|
||||
tenants = ['tranquility', 'singularity']
|
||||
|
||||
if not entity_id:
|
||||
raise ValueError('Invalid entity_id: {}'.format(entity_id))
|
||||
else:
|
||||
entity_id = int(entity_id)
|
||||
|
||||
if not size or size < 32 or size > 1024 or (size & (size - 1) != 0):
|
||||
raise ValueError('Invalid size: {}'.format(size))
|
||||
|
||||
if category not in categories:
|
||||
raise ValueError('Invalid category {}'.format(category))
|
||||
else:
|
||||
endpoint = categories[category]['endpoint']
|
||||
|
||||
if variant:
|
||||
if variant not in categories[category]['variants']:
|
||||
raise ValueError('Invalid variant {} for category {}'.format(
|
||||
variant,
|
||||
category
|
||||
))
|
||||
else:
|
||||
variant = categories[category]['variants'][0]
|
||||
|
||||
if tenant and tenant not in tenants:
|
||||
raise ValueError('Invalid tenant {}'.format(tenant))
|
||||
|
||||
# compose result URL
|
||||
result = '{}/{}/{}/{}?size={}'.format(
|
||||
_EVE_IMAGE_SERVER_URL,
|
||||
endpoint,
|
||||
entity_id,
|
||||
variant,
|
||||
size
|
||||
)
|
||||
if tenant:
|
||||
result += '&tenant={}'.format(tenant)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def alliance_logo_url(alliance_id: int, size: int = _DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for the given alliance ID"""
|
||||
return _eve_entity_image_url(_ESI_CATEGORY_ALLIANCE, alliance_id, size)
|
||||
|
||||
|
||||
def corporation_logo_url(
|
||||
corporation_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""image URL for the given corporation ID"""
|
||||
return _eve_entity_image_url(
|
||||
_ESI_CATEGORY_CORPORATION, corporation_id, size
|
||||
)
|
||||
|
||||
|
||||
def character_portrait_url(
|
||||
character_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""image URL for the given character ID"""
|
||||
return _eve_entity_image_url(_ESI_CATEGORY_CHARACTER, character_id, size)
|
||||
|
||||
|
||||
def type_icon_url(type_id: int, size: int = _DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""icon image URL for the given type ID"""
|
||||
return _eve_entity_image_url(
|
||||
_ESI_CATEGORY_INVENTORYTYPE, type_id, size, variant='icon'
|
||||
)
|
||||
|
||||
|
||||
def type_render_url(type_id: int, size: int = _DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""render image URL for the given type ID"""
|
||||
return _eve_entity_image_url(
|
||||
_ESI_CATEGORY_INVENTORYTYPE, type_id, size, variant='render'
|
||||
)
|
||||
@@ -1,22 +1,27 @@
|
||||
# this module generates profile URLs for evewho
|
||||
|
||||
from urllib.parse import urljoin, quote
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from . import *
|
||||
from . import (
|
||||
_ESI_CATEGORY_ALLIANCE,
|
||||
_ESI_CATEGORY_CORPORATION,
|
||||
_ESI_CATEGORY_CHARACTER,
|
||||
)
|
||||
|
||||
BASE_URL = 'https://evewho.com'
|
||||
|
||||
_BASE_URL = 'https://evewho.com'
|
||||
|
||||
|
||||
def _build_url(category: str, eve_id: int) -> str:
|
||||
"""return url to profile page for an eve entity"""
|
||||
|
||||
if category == ESI_CATEGORY_ALLIANCE:
|
||||
if category == _ESI_CATEGORY_ALLIANCE:
|
||||
partial = 'alliance'
|
||||
|
||||
elif category == ESI_CATEGORY_CORPORATION:
|
||||
elif category == _ESI_CATEGORY_CORPORATION:
|
||||
partial = 'corporation'
|
||||
|
||||
elif category == ESI_CATEGORY_CHARACTER:
|
||||
elif category == _ESI_CATEGORY_CHARACTER:
|
||||
partial = 'character'
|
||||
|
||||
else:
|
||||
@@ -25,7 +30,7 @@ def _build_url(category: str, eve_id: int) -> str:
|
||||
)
|
||||
|
||||
url = urljoin(
|
||||
BASE_URL,
|
||||
_BASE_URL,
|
||||
'{}/{}'.format(partial, int(eve_id))
|
||||
)
|
||||
return url
|
||||
@@ -33,12 +38,14 @@ def _build_url(category: str, eve_id: int) -> str:
|
||||
|
||||
def alliance_url(eve_id: int) -> str:
|
||||
"""url for page about given alliance on evewho"""
|
||||
return _build_url(ESI_CATEGORY_ALLIANCE, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_ALLIANCE, eve_id)
|
||||
|
||||
|
||||
def character_url(eve_id: int) -> str:
|
||||
"""url for page about given character on evewho"""
|
||||
return _build_url(ESI_CATEGORY_CHARACTER, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_CHARACTER, eve_id)
|
||||
|
||||
|
||||
def corporation_url(eve_id: int) -> str:
|
||||
"""url for page about given corporation on evewho"""
|
||||
return _build_url(ESI_CATEGORY_CORPORATION, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_CORPORATION, eve_id)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from ...models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from .. import dotlan, zkillboard, evewho
|
||||
from .. import dotlan, zkillboard, evewho, eveimageserver
|
||||
from ...templatetags import evelinks
|
||||
|
||||
|
||||
@@ -90,3 +90,115 @@ class TestZkillboard(TestCase):
|
||||
'https://zkillboard.com/system/12345678/'
|
||||
)
|
||||
|
||||
|
||||
class TestEveImageServer(TestCase):
|
||||
"""unit test for eveimageserver"""
|
||||
|
||||
def test_sizes(self):
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=32),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=64),
|
||||
'https://images.evetech.net/characters/42/portrait?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=128),
|
||||
'https://images.evetech.net/characters/42/portrait?size=128'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=256),
|
||||
'https://images.evetech.net/characters/42/portrait?size=256'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=512),
|
||||
'https://images.evetech.net/characters/42/portrait?size=512'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=1024),
|
||||
'https://images.evetech.net/characters/42/portrait?size=1024'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, size=-5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, size=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, size=31)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, size=1025)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, size=2048)
|
||||
|
||||
|
||||
def test_variant(self):
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, variant='portrait'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('alliance', 42, variant='logo'),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('character', 42, variant='logo')
|
||||
|
||||
|
||||
def test_alliance(self):
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('alliance', 42),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('corporation', 42),
|
||||
'https://images.evetech.net/corporations/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('station', 42)
|
||||
|
||||
|
||||
def test_tenants(self):
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, tenant='tranquility'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=tranquility'
|
||||
)
|
||||
self.assertEqual(
|
||||
eveimageserver._eve_entity_image_url('character', 42, tenant='singularity'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=singularity'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
eveimageserver._eve_entity_image_url('character', 42, tenant='xxx')
|
||||
|
||||
def test_alliance_logo_url(self):
|
||||
expected = 'https://images.evetech.net/alliances/42/logo?size=128'
|
||||
self.assertEqual(eveimageserver.alliance_logo_url(42, 128), expected)
|
||||
|
||||
def test_corporation_logo_url(self):
|
||||
expected = 'https://images.evetech.net/corporations/42/logo?size=128'
|
||||
self.assertEqual(eveimageserver.corporation_logo_url(42, 128), expected)
|
||||
|
||||
def test_character_portrait_url(self):
|
||||
expected = 'https://images.evetech.net/characters/42/portrait?size=128'
|
||||
self.assertEqual(
|
||||
eveimageserver.character_portrait_url(42, 128), expected
|
||||
)
|
||||
|
||||
def test_type_icon_url(self):
|
||||
expected = 'https://images.evetech.net/types/42/icon?size=128'
|
||||
self.assertEqual(eveimageserver.type_icon_url(42, 128), expected)
|
||||
|
||||
def test_type_render_url(self):
|
||||
expected = 'https://images.evetech.net/types/42/render?size=128'
|
||||
self.assertEqual(eveimageserver.type_render_url(42, 128), expected)
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from ...models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from .. import dotlan, zkillboard, evewho
|
||||
from .. import eveimageserver, evewho, dotlan, zkillboard
|
||||
from ...templatetags import evelinks
|
||||
|
||||
|
||||
@@ -332,3 +332,28 @@ class TestTemplateTags(TestCase):
|
||||
''
|
||||
)
|
||||
|
||||
def test_type_icon_url(self):
|
||||
expected = eveimageserver.type_icon_url(123)
|
||||
self.assertEqual(evelinks.type_icon_url(123), expected)
|
||||
|
||||
expected = eveimageserver.type_icon_url(123, 128)
|
||||
self.assertEqual(evelinks.type_icon_url(123, 128), expected)
|
||||
|
||||
expected = ''
|
||||
self.assertEqual(evelinks.type_icon_url(123, 99), expected)
|
||||
|
||||
expected = ''
|
||||
self.assertEqual(evelinks.type_icon_url(None), expected)
|
||||
|
||||
def test_type_render_url(self):
|
||||
expected = eveimageserver.type_render_url(123)
|
||||
self.assertEqual(evelinks.type_render_url(123), expected)
|
||||
|
||||
expected = eveimageserver.type_render_url(123, 128)
|
||||
self.assertEqual(evelinks.type_render_url(123, 128), expected)
|
||||
|
||||
expected = ''
|
||||
self.assertEqual(evelinks.type_render_url(123, 99), expected)
|
||||
|
||||
expected = ''
|
||||
self.assertEqual(evelinks.type_render_url(None), expected)
|
||||
@@ -1,28 +1,35 @@
|
||||
# this module generates profile URLs for zKillboard
|
||||
|
||||
from urllib.parse import urljoin, quote
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from . import *
|
||||
from . import (
|
||||
_ESI_CATEGORY_ALLIANCE,
|
||||
_ESI_CATEGORY_CORPORATION,
|
||||
_ESI_CATEGORY_CHARACTER,
|
||||
_ESI_CATEGORY_REGION,
|
||||
_ESI_CATEGORY_SOLARSYSTEM
|
||||
)
|
||||
|
||||
BASE_URL = 'https://zkillboard.com'
|
||||
|
||||
_BASE_URL = 'https://zkillboard.com'
|
||||
|
||||
|
||||
def _build_url(category: str, eve_id: int) -> str:
|
||||
"""return url to profile page for an eve entity"""
|
||||
|
||||
if category == ESI_CATEGORY_ALLIANCE:
|
||||
if category == _ESI_CATEGORY_ALLIANCE:
|
||||
partial = 'alliance'
|
||||
|
||||
elif category == ESI_CATEGORY_CORPORATION:
|
||||
elif category == _ESI_CATEGORY_CORPORATION:
|
||||
partial = 'corporation'
|
||||
|
||||
elif category == ESI_CATEGORY_CHARACTER:
|
||||
elif category == _ESI_CATEGORY_CHARACTER:
|
||||
partial = 'character'
|
||||
|
||||
elif category == ESI_CATEGORY_REGION:
|
||||
elif category == _ESI_CATEGORY_REGION:
|
||||
partial = 'region'
|
||||
|
||||
elif category == ESI_CATEGORY_SOLARSYSTEM:
|
||||
elif category == _ESI_CATEGORY_SOLARSYSTEM:
|
||||
partial = 'system'
|
||||
|
||||
else:
|
||||
@@ -31,7 +38,7 @@ def _build_url(category: str, eve_id: int) -> str:
|
||||
)
|
||||
|
||||
url = urljoin(
|
||||
BASE_URL,
|
||||
_BASE_URL,
|
||||
'{}/{}/'.format(partial, int(eve_id))
|
||||
)
|
||||
return url
|
||||
@@ -39,19 +46,23 @@ def _build_url(category: str, eve_id: int) -> str:
|
||||
|
||||
def alliance_url(eve_id: int) -> str:
|
||||
"""url for page about given alliance on zKillboard"""
|
||||
return _build_url(ESI_CATEGORY_ALLIANCE, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_ALLIANCE, eve_id)
|
||||
|
||||
|
||||
def character_url(eve_id: int) -> str:
|
||||
"""url for page about given character on zKillboard"""
|
||||
return _build_url(ESI_CATEGORY_CHARACTER, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_CHARACTER, eve_id)
|
||||
|
||||
|
||||
def corporation_url(eve_id: int) -> str:
|
||||
"""url for page about given corporation on zKillboard"""
|
||||
return _build_url(ESI_CATEGORY_CORPORATION, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_CORPORATION, eve_id)
|
||||
|
||||
|
||||
def region_url(eve_id: int) -> str:
|
||||
"""url for page about given region on zKillboard"""
|
||||
return _build_url(ESI_CATEGORY_REGION, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_REGION, eve_id)
|
||||
|
||||
|
||||
def solar_system_url(eve_id: int) -> str:
|
||||
return _build_url(ESI_CATEGORY_SOLARSYSTEM, eve_id)
|
||||
return _build_url(_ESI_CATEGORY_SOLARSYSTEM, eve_id)
|
||||
|
||||
@@ -89,4 +89,6 @@ class EveCorporationManager(models.Manager):
|
||||
)
|
||||
|
||||
def update_corporation(self, corp_id):
|
||||
return self.get(corporation_id=corp_id).update_corporation(self.provider.get_corporation(corp_id))
|
||||
return self\
|
||||
.get(corporation_id=corp_id)\
|
||||
.update_corporation(self.provider.get_corporation(corp_id))
|
||||
|
||||
43
allianceauth/eveonline/migrations/0011_ids_to_integers.py
Normal file
43
allianceauth/eveonline/migrations/0011_ids_to_integers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 2.2.12 on 2020-05-25 02:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eveonline', '0010_alliance_ticker'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eveallianceinfo',
|
||||
name='alliance_id',
|
||||
field=models.PositiveIntegerField(unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eveallianceinfo',
|
||||
name='executor_corp_id',
|
||||
field=models.PositiveIntegerField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='evecharacter',
|
||||
name='alliance_id',
|
||||
field=models.PositiveIntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='evecharacter',
|
||||
name='character_id',
|
||||
field=models.PositiveIntegerField(unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='evecharacter',
|
||||
name='corporation_id',
|
||||
field=models.PositiveIntegerField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='evecorporationinfo',
|
||||
name='corporation_id',
|
||||
field=models.PositiveIntegerField(unique=True),
|
||||
),
|
||||
]
|
||||
33
allianceauth/eveonline/migrations/0012_index_additions.py
Normal file
33
allianceauth/eveonline/migrations/0012_index_additions.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.12 on 2020-05-26 02:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eveonline', '0011_ids_to_integers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='eveallianceinfo',
|
||||
index=models.Index(fields=['executor_corp_id'], name='eveonline_e_executo_7f3280_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='evecharacter',
|
||||
index=models.Index(fields=['corporation_id'], name='eveonline_e_corpora_cb4cd9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='evecharacter',
|
||||
index=models.Index(fields=['alliance_id'], name='eveonline_e_allianc_39ee2a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='evecharacter',
|
||||
index=models.Index(fields=['corporation_name'], name='eveonline_e_corpora_893c60_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='evecharacter',
|
||||
index=models.Index(fields=['alliance_name'], name='eveonline_e_allianc_63fd98_idx'),
|
||||
),
|
||||
]
|
||||
@@ -5,109 +5,35 @@ from .managers import EveCharacterManager, EveCharacterProviderManager
|
||||
from .managers import EveCorporationManager, EveCorporationProviderManager
|
||||
from .managers import EveAllianceManager, EveAllianceProviderManager
|
||||
from . import providers
|
||||
from .evelinks import eveimageserver
|
||||
|
||||
|
||||
EVE_IMAGE_SERVER_URL = 'https://images.evetech.net'
|
||||
|
||||
|
||||
def _eve_entity_image_url(
|
||||
category: str,
|
||||
id: int,
|
||||
size: int = 32,
|
||||
variant: str = None,
|
||||
tenant: str = None,
|
||||
) -> str:
|
||||
"""returns image URL for an Eve Online ID.
|
||||
Supported categories: `alliance`, `corporation`, `character`
|
||||
|
||||
Arguments:
|
||||
- category: category of the ID
|
||||
- id: Eve ID of the entity
|
||||
- size: (optional) render size of the image.must be between 32 (default) and 1024
|
||||
- variant: (optional) image variant for category. currently not relevant.
|
||||
- tentant: (optional) Eve Server, either `tranquility`(default) or `singularity`
|
||||
|
||||
Returns:
|
||||
- URL string for the requested image on the Eve image server
|
||||
|
||||
Exceptions:
|
||||
- Throws ValueError on invalid input
|
||||
"""
|
||||
|
||||
# input validations
|
||||
categories = {
|
||||
'alliance': {
|
||||
'endpoint': 'alliances',
|
||||
'variants': [
|
||||
'logo'
|
||||
]
|
||||
},
|
||||
'corporation': {
|
||||
'endpoint': 'corporations',
|
||||
'variants': [
|
||||
'logo'
|
||||
]
|
||||
},
|
||||
'character': {
|
||||
'endpoint': 'characters',
|
||||
'variants': [
|
||||
'portrait'
|
||||
]
|
||||
}
|
||||
}
|
||||
tenants = ['tranquility', 'singularity']
|
||||
|
||||
if size < 32 or size > 1024 or (size & (size - 1) != 0):
|
||||
raise ValueError('Invalid size: {}'.format(size))
|
||||
|
||||
if category not in categories:
|
||||
raise ValueError('Invalid category {}'.format(category))
|
||||
else:
|
||||
endpoint = categories[category]['endpoint']
|
||||
|
||||
if variant:
|
||||
if variant not in categories[category]['variants']:
|
||||
raise ValueError('Invalid variant {} for category {}'.format(
|
||||
variant,
|
||||
category
|
||||
))
|
||||
else:
|
||||
variant = categories[category]['variants'][0]
|
||||
|
||||
if tenant and tenant not in tenants:
|
||||
raise ValueError('Invalid tentant {}'.format(tenant))
|
||||
|
||||
# compose result URL
|
||||
result = '{}/{}/{}/{}?size={}'.format(
|
||||
EVE_IMAGE_SERVER_URL,
|
||||
endpoint,
|
||||
id,
|
||||
variant,
|
||||
size
|
||||
)
|
||||
if tenant:
|
||||
result += '&tenant={}'.format(tenant)
|
||||
|
||||
return result
|
||||
_DEFAULT_IMAGE_SIZE = 32
|
||||
|
||||
|
||||
class EveAllianceInfo(models.Model):
|
||||
alliance_id = models.CharField(max_length=254, unique=True)
|
||||
alliance_id = models.PositiveIntegerField(unique=True)
|
||||
alliance_name = models.CharField(max_length=254, unique=True)
|
||||
alliance_ticker = models.CharField(max_length=254)
|
||||
executor_corp_id = models.CharField(max_length=254)
|
||||
executor_corp_id = models.PositiveIntegerField()
|
||||
|
||||
objects = EveAllianceManager()
|
||||
provider = EveAllianceProviderManager()
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=['executor_corp_id',])]
|
||||
|
||||
def populate_alliance(self):
|
||||
alliance = self.provider.get_alliance(self.alliance_id)
|
||||
for corp_id in alliance.corp_ids:
|
||||
if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
|
||||
EveCorporationInfo.objects.create_corporation(corp_id)
|
||||
EveCorporationInfo.objects.filter(corporation_id__in=alliance.corp_ids).update(alliance=self)
|
||||
EveCorporationInfo.objects.filter(alliance=self).exclude(corporation_id__in=alliance.corp_ids).update(
|
||||
alliance=None)
|
||||
EveCorporationInfo.objects.filter(
|
||||
corporation_id__in=alliance.corp_ids).update(alliance=self
|
||||
)
|
||||
EveCorporationInfo.objects\
|
||||
.filter(alliance=self)\
|
||||
.exclude(corporation_id__in=alliance.corp_ids)\
|
||||
.update(alliance=None)
|
||||
|
||||
def update_alliance(self, alliance: providers.Alliance = None):
|
||||
if alliance is None:
|
||||
@@ -120,11 +46,13 @@ class EveAllianceInfo(models.Model):
|
||||
return self.alliance_name
|
||||
|
||||
@staticmethod
|
||||
def generic_logo_url(alliance_id: int, size: int = 32) -> str:
|
||||
def generic_logo_url(
|
||||
alliance_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""image URL for the given alliance ID"""
|
||||
return _eve_entity_image_url('alliance', alliance_id, size)
|
||||
return eveimageserver.alliance_logo_url(alliance_id, size)
|
||||
|
||||
def logo_url(self, size:int = 32) -> str:
|
||||
def logo_url(self, size: int = _DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL of this alliance"""
|
||||
return self.generic_logo_url(self.alliance_id, size)
|
||||
|
||||
@@ -150,11 +78,13 @@ class EveAllianceInfo(models.Model):
|
||||
|
||||
|
||||
class EveCorporationInfo(models.Model):
|
||||
corporation_id = models.CharField(max_length=254, unique=True)
|
||||
corporation_id = models.PositiveIntegerField(unique=True)
|
||||
corporation_name = models.CharField(max_length=254, unique=True)
|
||||
corporation_ticker = models.CharField(max_length=254)
|
||||
member_count = models.IntegerField()
|
||||
alliance = models.ForeignKey(EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL)
|
||||
alliance = models.ForeignKey(
|
||||
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
objects = EveCorporationManager()
|
||||
provider = EveCorporationProviderManager()
|
||||
@@ -174,11 +104,13 @@ class EveCorporationInfo(models.Model):
|
||||
return self.corporation_name
|
||||
|
||||
@staticmethod
|
||||
def generic_logo_url(corporation_id: int, size: int = 32) -> str:
|
||||
def generic_logo_url(
|
||||
corporation_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""image URL for the given corporation ID"""
|
||||
return _eve_entity_image_url('corporation', corporation_id, size)
|
||||
return eveimageserver.corporation_logo_url(corporation_id, size)
|
||||
|
||||
def logo_url(self, size:int = 32) -> str:
|
||||
def logo_url(self, size: int = _DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.generic_logo_url(self.corporation_id, size)
|
||||
|
||||
@@ -204,18 +136,26 @@ class EveCorporationInfo(models.Model):
|
||||
|
||||
|
||||
class EveCharacter(models.Model):
|
||||
character_id = models.CharField(max_length=254, unique=True)
|
||||
character_id = models.PositiveIntegerField(unique=True)
|
||||
character_name = models.CharField(max_length=254, unique=True)
|
||||
corporation_id = models.CharField(max_length=254)
|
||||
corporation_id = models.PositiveIntegerField()
|
||||
corporation_name = models.CharField(max_length=254)
|
||||
corporation_ticker = models.CharField(max_length=5)
|
||||
alliance_id = models.CharField(max_length=254, blank=True, null=True, default='')
|
||||
alliance_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
||||
alliance_name = models.CharField(max_length=254, blank=True, null=True, default='')
|
||||
alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='')
|
||||
|
||||
objects = EveCharacterManager()
|
||||
provider = EveCharacterProviderManager()
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['corporation_id',]),
|
||||
models.Index(fields=['alliance_id',]),
|
||||
models.Index(fields=['corporation_name',]),
|
||||
models.Index(fields=['alliance_name',]),
|
||||
]
|
||||
|
||||
@property
|
||||
def alliance(self) -> Union[EveAllianceInfo, None]:
|
||||
"""
|
||||
@@ -253,11 +193,13 @@ class EveCharacter(models.Model):
|
||||
return self.character_name
|
||||
|
||||
@staticmethod
|
||||
def generic_portrait_url(character_id: int, size: int = 32) -> str:
|
||||
def generic_portrait_url(
|
||||
character_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""image URL for the given character ID"""
|
||||
return _eve_entity_image_url('character', character_id, size)
|
||||
return eveimageserver.character_portrait_url(character_id, size)
|
||||
|
||||
def portrait_url(self, size = 32) -> str:
|
||||
def portrait_url(self, size=_DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.generic_portrait_url(self.character_id, size)
|
||||
|
||||
@@ -281,7 +223,7 @@ class EveCharacter(models.Model):
|
||||
"""image URL for this character"""
|
||||
return self.portrait_url(256)
|
||||
|
||||
def corporation_logo_url(self, size = 32) -> str:
|
||||
def corporation_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return EveCorporationInfo.generic_logo_url(self.corporation_id, size)
|
||||
|
||||
@@ -305,7 +247,7 @@ class EveCharacter(models.Model):
|
||||
"""image URL for corporation of this character"""
|
||||
return self.corporation_logo_url(256)
|
||||
|
||||
def alliance_logo_url(self, size = 32) -> str:
|
||||
def alliance_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
if self.alliance_id:
|
||||
return EveAllianceInfo.generic_logo_url(self.alliance_id, size)
|
||||
|
||||
@@ -5,35 +5,96 @@ from .models import EveAllianceInfo
|
||||
from .models import EveCharacter
|
||||
from .models import EveCorporationInfo
|
||||
|
||||
from . import providers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TASK_PRIORITY = 7
|
||||
CHUNK_SIZE = 500
|
||||
|
||||
|
||||
def chunks(lst, n):
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i:i + n]
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_corp(corp_id):
|
||||
"""Update given corporation from ESI"""
|
||||
EveCorporationInfo.objects.update_corporation(corp_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_alliance(alliance_id):
|
||||
"""Update given alliance from ESI"""
|
||||
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_character(character_id):
|
||||
"""Update given character from ESI"""
|
||||
EveCharacter.objects.update_character(character_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
def run_model_update():
|
||||
"""Update all alliances, corporations and characters from ESI"""
|
||||
|
||||
# update existing corp models
|
||||
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
||||
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
||||
update_corp.apply_async(
|
||||
args=[corp['corporation_id']], priority=TASK_PRIORITY
|
||||
)
|
||||
|
||||
# update existing alliance models
|
||||
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
||||
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
|
||||
update_alliance.apply_async(
|
||||
args=[alliance['alliance_id']], priority=TASK_PRIORITY
|
||||
)
|
||||
|
||||
#update existing character models
|
||||
for character in EveCharacter.objects.all().values('character_id'):
|
||||
update_character.apply_async(args=[character['character_id']], priority=TASK_PRIORITY)
|
||||
# update existing character models
|
||||
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
|
||||
for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
|
||||
affiliations_raw = providers.provider.client.Character\
|
||||
.post_characters_affiliation(characters=character_ids_chunk).result()
|
||||
character_names = providers.provider.client.Universe\
|
||||
.post_universe_names(ids=character_ids_chunk).result()
|
||||
|
||||
affiliations = {
|
||||
affiliation.get('character_id'): affiliation
|
||||
for affiliation in affiliations_raw
|
||||
}
|
||||
# add character names to affiliations
|
||||
for character in character_names:
|
||||
character_id = character.get('id')
|
||||
if character_id in affiliations:
|
||||
affiliations[character_id]['name'] = character.get('name')
|
||||
|
||||
# fetch current characters
|
||||
characters = EveCharacter.objects.filter(character_id__in=character_ids_chunk)\
|
||||
.values('character_id', 'corporation_id', 'alliance_id', 'character_name')
|
||||
|
||||
for character in characters:
|
||||
character_id = character.get('character_id')
|
||||
if character_id in affiliations:
|
||||
affiliation = affiliations[character_id]
|
||||
|
||||
corp_changed = (
|
||||
character.get('corporation_id') != affiliation.get('corporation_id')
|
||||
)
|
||||
|
||||
alliance_id = character.get('alliance_id')
|
||||
if not alliance_id:
|
||||
alliance_id = None
|
||||
alliance_changed = alliance_id != affiliation.get('alliance_id')
|
||||
|
||||
name_changed = False
|
||||
fetched_name = affiliation.get('name', False)
|
||||
if fetched_name:
|
||||
name_changed = character.get('character_name') != fetched_name
|
||||
|
||||
if corp_changed or alliance_changed or name_changed:
|
||||
update_character.apply_async(
|
||||
args=[character.get('character_id')], priority=TASK_PRIORITY
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from django import template
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..evelinks import evewho, dotlan, zkillboard
|
||||
from ..evelinks import eveimageserver, evewho, dotlan, zkillboard
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -163,7 +163,7 @@ def dotlan_solar_system_url(eve_obj: object) -> str:
|
||||
return _generic_evelinks_url(dotlan, 'solar_system_url', eve_obj)
|
||||
|
||||
|
||||
#zkillboard
|
||||
# zkillboard
|
||||
|
||||
@register.filter
|
||||
def zkillboard_character_url(eve_obj: EveCharacter) -> str:
|
||||
@@ -212,7 +212,6 @@ def zkillboard_solar_system_url(eve_obj: object) -> str:
|
||||
|
||||
# image urls
|
||||
|
||||
|
||||
@register.filter
|
||||
def character_portrait_url(
|
||||
eve_obj: object,
|
||||
@@ -284,3 +283,30 @@ def alliance_logo_url(
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
|
||||
@register.filter
|
||||
def type_icon_url(
|
||||
type_id: int,
|
||||
size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""generates a icon image URL for the given type ID
|
||||
Returns URL or empty string
|
||||
"""
|
||||
try:
|
||||
return eveimageserver.type_icon_url(type_id, size)
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
|
||||
@register.filter
|
||||
def type_render_url(
|
||||
type_id: int,
|
||||
size: int = _DEFAULT_IMAGE_SIZE
|
||||
) -> str:
|
||||
"""generates a render image URL for the given type ID
|
||||
Returns URL or empty string
|
||||
"""
|
||||
try:
|
||||
return eveimageserver.type_render_url(type_id, size)
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
@@ -12,7 +12,7 @@ class EveCharacterProviderManagerTestCase(TestCase):
|
||||
expected = Character()
|
||||
provider.get_character.return_value = expected
|
||||
|
||||
result = EveCharacter.provider.get_character('1234')
|
||||
result = EveCharacter.provider.get_character(1234)
|
||||
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@@ -22,30 +22,30 @@ class EveCharacterManagerTestCase(TestCase):
|
||||
class TestCharacter(Character):
|
||||
@property
|
||||
def alliance(self):
|
||||
return Alliance(id='3456', name='Test Alliance')
|
||||
return Alliance(id=3456, name='Test Alliance')
|
||||
|
||||
@property
|
||||
def corp(self):
|
||||
return Corporation(
|
||||
id='2345',
|
||||
id=2345,
|
||||
name='Test Corp',
|
||||
alliance_id='3456',
|
||||
ticker='0BUGS'
|
||||
alliance_id=3456,
|
||||
ticker='0BUGS' #lies, blatant lies!
|
||||
)
|
||||
|
||||
@mock.patch('allianceauth.eveonline.managers.providers.provider')
|
||||
def test_create_character(self, provider):
|
||||
# Also covers create_character_obj
|
||||
expected = self.TestCharacter(
|
||||
id='1234',
|
||||
id=1234,
|
||||
name='Test Character',
|
||||
corp_id='2345',
|
||||
alliance_id='3456'
|
||||
corp_id=2345,
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
provider.get_character.return_value = expected
|
||||
|
||||
result = EveCharacter.objects.create_character('1234')
|
||||
result = EveCharacter.objects.create_character(1234)
|
||||
|
||||
self.assertEqual(result.character_id, expected.id)
|
||||
self.assertEqual(result.character_name, expected.name)
|
||||
@@ -59,25 +59,24 @@ class EveCharacterManagerTestCase(TestCase):
|
||||
def test_update_character(self, provider):
|
||||
# Also covers Model.update_character
|
||||
existing = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='character.corp.id',
|
||||
corporation_id=23457,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='character.alliance.id',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=34567,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
expected = self.TestCharacter(
|
||||
id='1234',
|
||||
id=1234,
|
||||
name='Test Character',
|
||||
corp_id='2345',
|
||||
alliance_id='3456'
|
||||
corp_id=2345,
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
provider.get_character.return_value = expected
|
||||
|
||||
result = EveCharacter.objects.update_character('1234')
|
||||
result = EveCharacter.objects.update_character(1234)
|
||||
|
||||
self.assertEqual(result.character_id, expected.id)
|
||||
self.assertEqual(result.character_name, expected.name)
|
||||
@@ -90,23 +89,23 @@ class EveCharacterManagerTestCase(TestCase):
|
||||
def test_get_character_by_id(self):
|
||||
EveCharacter.objects.all().delete()
|
||||
EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='character.corp.id',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='character.alliance.id',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
# try to get existing character
|
||||
result = EveCharacter.objects.get_character_by_id('1234')
|
||||
result = EveCharacter.objects.get_character_by_id(1234)
|
||||
|
||||
self.assertEqual(result.character_id, '1234')
|
||||
self.assertEqual(result.character_id, 1234)
|
||||
self.assertEqual(result.character_name, 'character.name')
|
||||
|
||||
# try to get non existing character
|
||||
self.assertIsNone(EveCharacter.objects.get_character_by_id('9999'))
|
||||
self.assertIsNone(EveCharacter.objects.get_character_by_id(9999))
|
||||
|
||||
|
||||
class EveAllianceProviderManagerTestCase(TestCase):
|
||||
@@ -115,7 +114,7 @@ class EveAllianceProviderManagerTestCase(TestCase):
|
||||
expected = Alliance()
|
||||
provider.get_alliance.return_value = expected
|
||||
|
||||
result = EveAllianceInfo.provider.get_alliance('1234')
|
||||
result = EveAllianceInfo.provider.get_alliance(1234)
|
||||
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@@ -131,16 +130,16 @@ class EveAllianceManagerTestCase(TestCase):
|
||||
def test_create_alliance(self, provider, populate_alliance):
|
||||
# Also covers create_alliance_obj
|
||||
expected = self.TestAlliance(
|
||||
id='3456',
|
||||
id=3456,
|
||||
name='Test Alliance',
|
||||
ticker='TEST',
|
||||
corp_ids=['2345'],
|
||||
executor_corp_id='2345'
|
||||
corp_ids=[2345],
|
||||
executor_corp_id=2345
|
||||
)
|
||||
|
||||
provider.get_alliance.return_value = expected
|
||||
|
||||
result = EveAllianceInfo.objects.create_alliance('3456')
|
||||
result = EveAllianceInfo.objects.create_alliance(3456)
|
||||
|
||||
self.assertEqual(result.alliance_id, expected.id)
|
||||
self.assertEqual(result.alliance_name, expected.name)
|
||||
@@ -152,22 +151,22 @@ class EveAllianceManagerTestCase(TestCase):
|
||||
def test_update_alliance(self, provider):
|
||||
# Also covers Model.update_alliance
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='alliance.ticker',
|
||||
executor_corp_id='alliance.executor_corp_id',
|
||||
alliance_ticker='at1',
|
||||
executor_corp_id=2345,
|
||||
)
|
||||
expected = self.TestAlliance(
|
||||
id='3456',
|
||||
id=3456,
|
||||
name='Test Alliance',
|
||||
ticker='TEST',
|
||||
corp_ids=['2345'],
|
||||
executor_corp_id='2345'
|
||||
corp_ids=[2345],
|
||||
executor_corp_id=2345
|
||||
)
|
||||
|
||||
provider.get_alliance.return_value = expected
|
||||
|
||||
result = EveAllianceInfo.objects.update_alliance('3456')
|
||||
result = EveAllianceInfo.objects.update_alliance(3456)
|
||||
|
||||
# This is the only thing ever updated in code
|
||||
self.assertEqual(result.executor_corp_id, expected.executor_corp_id)
|
||||
@@ -179,7 +178,7 @@ class EveCorporationProviderManagerTestCase(TestCase):
|
||||
expected = Corporation()
|
||||
provider.get_corp.return_value = expected
|
||||
|
||||
result = EveCorporationInfo.provider.get_corporation('2345')
|
||||
result = EveCorporationInfo.provider.get_corporation(2345)
|
||||
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@@ -190,39 +189,39 @@ class EveCorporationManagerTestCase(TestCase):
|
||||
@property
|
||||
def alliance(self):
|
||||
return EveAllianceManagerTestCase.TestAlliance(
|
||||
id='3456',
|
||||
id=3456,
|
||||
name='Test Alliance',
|
||||
ticker='TEST',
|
||||
corp_ids=['2345'],
|
||||
executor_corp_id='2345'
|
||||
corp_ids=[2345],
|
||||
executor_corp_id=2345
|
||||
)
|
||||
|
||||
@property
|
||||
def ceo(self):
|
||||
return EveCharacterManagerTestCase.TestCharacter(
|
||||
id='1234',
|
||||
id=1234,
|
||||
name='Test Character',
|
||||
corp_id='2345',
|
||||
alliance_id='3456'
|
||||
corp_id=2345,
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
@mock.patch('allianceauth.eveonline.managers.providers.provider')
|
||||
def test_create_corporation(self, provider):
|
||||
# Also covers create_corp_obj
|
||||
exp_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='alliance.ticker',
|
||||
executor_corp_id='alliance.executor_corp_id',
|
||||
alliance_ticker='99bug',
|
||||
executor_corp_id=2345,
|
||||
)
|
||||
|
||||
expected = self.TestCorporation(
|
||||
id='2345',
|
||||
id=2345,
|
||||
name='Test Corp',
|
||||
ticker='0BUGS',
|
||||
ceo_id='1234',
|
||||
ceo_id=1234,
|
||||
members=1,
|
||||
alliance_id='3456'
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
provider.get_corp.return_value = expected
|
||||
@@ -240,17 +239,17 @@ class EveCorporationManagerTestCase(TestCase):
|
||||
# variant to test no alliance case
|
||||
# Also covers create_corp_obj
|
||||
expected = self.TestCorporation(
|
||||
id='2345',
|
||||
id=2345,
|
||||
name='Test Corp',
|
||||
ticker='0BUGS',
|
||||
ceo_id='1234',
|
||||
ceo_id=1234,
|
||||
members=1,
|
||||
alliance_id='3456'
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
provider.get_corp.return_value = expected
|
||||
|
||||
result = EveCorporationInfo.objects.create_corporation('2345')
|
||||
result = EveCorporationInfo.objects.create_corporation(2345)
|
||||
|
||||
self.assertEqual(result.corporation_id, expected.id)
|
||||
self.assertEqual(result.corporation_name, expected.name)
|
||||
@@ -262,27 +261,27 @@ class EveCorporationManagerTestCase(TestCase):
|
||||
def test_update_corporation(self, provider):
|
||||
# Also covers Model.update_corporation
|
||||
exp_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='alliance.ticker',
|
||||
executor_corp_id='alliance.executor_corp_id',
|
||||
alliance_ticker='at1',
|
||||
executor_corp_id=2345,
|
||||
)
|
||||
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='corp.name',
|
||||
corporation_ticker='corp.ticker',
|
||||
corporation_ticker='cc1',
|
||||
member_count=10,
|
||||
alliance=None,
|
||||
)
|
||||
|
||||
expected = self.TestCorporation(
|
||||
id='2345',
|
||||
id=2345,
|
||||
name='Test Corp',
|
||||
ticker='0BUGS',
|
||||
ceo_id='1234',
|
||||
ceo_id=1234,
|
||||
members=1,
|
||||
alliance_id='3456'
|
||||
alliance_id=3456
|
||||
)
|
||||
|
||||
provider.get_corp.return_value = expected
|
||||
|
||||
@@ -2,130 +2,40 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, \
|
||||
EveAllianceInfo, _eve_entity_image_url
|
||||
from ..models import (
|
||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
)
|
||||
from ..providers import Alliance, Corporation, Character
|
||||
from ..evelinks import eveimageserver
|
||||
|
||||
|
||||
class EveUniverseImageUrlTestCase(TestCase):
|
||||
"""unit test for _eve_entity_image_url()"""
|
||||
|
||||
def test_sizes(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=32),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=64),
|
||||
'https://images.evetech.net/characters/42/portrait?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=128),
|
||||
'https://images.evetech.net/characters/42/portrait?size=128'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=256),
|
||||
'https://images.evetech.net/characters/42/portrait?size=256'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=512),
|
||||
'https://images.evetech.net/characters/42/portrait?size=512'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=1024),
|
||||
'https://images.evetech.net/characters/42/portrait?size=1024'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=-5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=31)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=1025)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=2048)
|
||||
|
||||
|
||||
def test_variant(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, variant='portrait'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('alliance', 42, variant='logo'),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('character', 42, variant='logo')
|
||||
|
||||
|
||||
def test_alliance(self):
|
||||
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('alliance', 42),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('corporation', 42),
|
||||
'https://images.evetech.net/corporations/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('station', 42)
|
||||
|
||||
|
||||
def test_tenants(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, tenant='tranquility'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=tranquility'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, tenant='singularity'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=singularity'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('character', 42, tenant='xxx')
|
||||
|
||||
|
||||
class EveCharacterTestCase(TestCase):
|
||||
def test_corporation_prop(self):
|
||||
"""
|
||||
Test that the correct corporation is returned by the corporation property
|
||||
"""
|
||||
character = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='character.alliance.id',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=12345,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
expected = EveCorporationInfo.objects.create(
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='corp.name',
|
||||
corporation_ticker='corp.ticker',
|
||||
corporation_ticker='cc1',
|
||||
member_count=10,
|
||||
alliance=None,
|
||||
)
|
||||
|
||||
incorrect = EveCorporationInfo.objects.create(
|
||||
corporation_id='9999',
|
||||
corporation_id=9999,
|
||||
corporation_name='corp.name1',
|
||||
corporation_ticker='corp.ticker1',
|
||||
corporation_ticker='cc11',
|
||||
member_count=10,
|
||||
alliance=None,
|
||||
)
|
||||
@@ -139,44 +49,44 @@ class EveCharacterTestCase(TestCase):
|
||||
object is not in the database
|
||||
"""
|
||||
character = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='character.alliance.id',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=123456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
with self.assertRaises(EveCorporationInfo.DoesNotExist):
|
||||
result = character.corporation
|
||||
character.corporation
|
||||
|
||||
def test_alliance_prop(self):
|
||||
"""
|
||||
Test that the correct alliance is returned by the alliance property
|
||||
"""
|
||||
character = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='3456',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
expected = EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='alliance.ticker',
|
||||
executor_corp_id='alliance.executor_corp_id',
|
||||
alliance_ticker='ac2',
|
||||
executor_corp_id=2345,
|
||||
)
|
||||
|
||||
incorrect = EveAllianceInfo.objects.create(
|
||||
alliance_id='9001',
|
||||
alliance_id=9001,
|
||||
alliance_name='alliance.name1',
|
||||
alliance_ticker='alliance.ticker1',
|
||||
executor_corp_id='alliance.executor_corp_id1',
|
||||
alliance_ticker='ac1',
|
||||
executor_corp_id=2654,
|
||||
)
|
||||
|
||||
self.assertEqual(character.alliance, expected)
|
||||
@@ -188,28 +98,28 @@ class EveCharacterTestCase(TestCase):
|
||||
object is not in the database
|
||||
"""
|
||||
character = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
alliance_id='3456',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
|
||||
with self.assertRaises(EveAllianceInfo.DoesNotExist):
|
||||
result = character.alliance
|
||||
character.alliance
|
||||
|
||||
def test_alliance_prop_none(self):
|
||||
"""
|
||||
Check that None is returned when the character has no alliance
|
||||
"""
|
||||
character = EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_id=1234,
|
||||
character_name='character.name',
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='character.corp.ticker',
|
||||
corporation_ticker='cc1',
|
||||
alliance_id=None,
|
||||
alliance_name=None,
|
||||
)
|
||||
@@ -227,12 +137,12 @@ class EveCharacterTestCase(TestCase):
|
||||
)
|
||||
|
||||
my_character = EveCharacter.objects.create(
|
||||
character_id='1001',
|
||||
character_id=1001,
|
||||
character_name='Bruce Wayne',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Dummy Corp 1',
|
||||
corporation_ticker='DC1',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
)
|
||||
my_updated_character = Character(
|
||||
@@ -244,90 +154,87 @@ class EveCharacterTestCase(TestCase):
|
||||
|
||||
# todo: add test cases not yet covered, e.g. with alliance
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveCharacter.generic_portrait_url(42),
|
||||
_eve_entity_image_url('character', 42)
|
||||
eveimageserver._eve_entity_image_url('character', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveCharacter.generic_portrait_url(42, 256),
|
||||
_eve_entity_image_url('character', 42, 256)
|
||||
eveimageserver._eve_entity_image_url('character', 42, 256)
|
||||
)
|
||||
|
||||
def test_portrait_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_id=42,
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_id=123,
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url(),
|
||||
_eve_entity_image_url('character', 42)
|
||||
eveimageserver._eve_entity_image_url('character', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url(64),
|
||||
_eve_entity_image_url('character', 42, size=64)
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_32,
|
||||
_eve_entity_image_url('character', 42, size=32)
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_64,
|
||||
_eve_entity_image_url('character', 42, size=64)
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_128,
|
||||
_eve_entity_image_url('character', 42, size=128)
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_256,
|
||||
_eve_entity_image_url('character', 42, size=256)
|
||||
eveimageserver._eve_entity_image_url('character', 42, size=256)
|
||||
)
|
||||
|
||||
|
||||
def test_corporation_logo_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_id=42,
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_id=123,
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url(),
|
||||
_eve_entity_image_url('corporation', 123)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url(256),
|
||||
_eve_entity_image_url('corporation', 123, size=256)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123, size=256)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_32,
|
||||
_eve_entity_image_url('corporation', 123, size=32)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_64,
|
||||
_eve_entity_image_url('corporation', 123, size=64)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_128,
|
||||
_eve_entity_image_url('corporation', 123, size=128)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_256,
|
||||
_eve_entity_image_url('corporation', 123, size=256)
|
||||
eveimageserver._eve_entity_image_url('corporation', 123, size=256)
|
||||
)
|
||||
|
||||
|
||||
def test_alliance_logo_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_id=42,
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_id=123,
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
@@ -354,27 +261,27 @@ class EveCharacterTestCase(TestCase):
|
||||
x.alliance_id = 987
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url(),
|
||||
_eve_entity_image_url('alliance', 987)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url(128),
|
||||
_eve_entity_image_url('alliance', 987, size=128)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_32,
|
||||
_eve_entity_image_url('alliance', 987, size=32)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_64,
|
||||
_eve_entity_image_url('alliance', 987, size=64)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_128,
|
||||
_eve_entity_image_url('alliance', 987, size=128)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_256,
|
||||
_eve_entity_image_url('alliance', 987, size=256)
|
||||
eveimageserver._eve_entity_image_url('alliance', 987, size=256)
|
||||
)
|
||||
|
||||
|
||||
@@ -456,7 +363,6 @@ class EveAllianceTestCase(TestCase):
|
||||
# potential bug
|
||||
# update_alliance() is only updateting executor_corp_id when object is given
|
||||
|
||||
|
||||
def test_update_alliance_wo_object(self):
|
||||
mock_EveAllianceProviderManager = Mock()
|
||||
mock_EveAllianceProviderManager.get_alliance.return_value = \
|
||||
@@ -475,11 +381,11 @@ class EveAllianceTestCase(TestCase):
|
||||
)
|
||||
my_alliance.provider = mock_EveAllianceProviderManager
|
||||
my_alliance.save()
|
||||
updated_alliance = Alliance(
|
||||
name='Dummy Alliance 2',
|
||||
corp_ids=[2004],
|
||||
executor_corp_id=2004
|
||||
)
|
||||
Alliance(
|
||||
name='Dummy Alliance 2',
|
||||
corp_ids=[2004],
|
||||
executor_corp_id=2004
|
||||
)
|
||||
my_alliance.update_alliance()
|
||||
my_alliance.refresh_from_db()
|
||||
self.assertEqual(int(my_alliance.executor_corp_id), 2004)
|
||||
@@ -487,23 +393,22 @@ class EveAllianceTestCase(TestCase):
|
||||
# potential bug
|
||||
# update_alliance() is only updateting executor_corp_id nothing else ???
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveAllianceInfo.generic_logo_url(42),
|
||||
_eve_entity_image_url('alliance', 42)
|
||||
eveimageserver._eve_entity_image_url('alliance', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveAllianceInfo.generic_logo_url(42, 256),
|
||||
_eve_entity_image_url('alliance', 42, 256)
|
||||
eveimageserver._eve_entity_image_url('alliance', 42, 256)
|
||||
)
|
||||
|
||||
def test_logo_url(self):
|
||||
x = EveAllianceInfo(
|
||||
alliance_id='42',
|
||||
alliance_id=42,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='ABC',
|
||||
executor_corp_id='123'
|
||||
executor_corp_id=123
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url(),
|
||||
@@ -563,9 +468,7 @@ class EveCorporationTestCase(TestCase):
|
||||
|
||||
def test_update_corporation_no_object_w_alliance(self):
|
||||
mock_provider = Mock()
|
||||
mock_provider.get_corporation.return_value = Corporation(
|
||||
members=87
|
||||
)
|
||||
mock_provider.get_corporation.return_value = Corporation(members=87)
|
||||
self.my_corp.provider = mock_provider
|
||||
|
||||
self.my_corp.update_corporation()
|
||||
@@ -585,15 +488,14 @@ class EveCorporationTestCase(TestCase):
|
||||
self.assertEqual(my_corp2.member_count, 8)
|
||||
self.assertIsNone(my_corp2.alliance)
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveCorporationInfo.generic_logo_url(42),
|
||||
_eve_entity_image_url('corporation', 42)
|
||||
eveimageserver._eve_entity_image_url('corporation', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveCorporationInfo.generic_logo_url(42, 256),
|
||||
_eve_entity_image_url('corporation', 42, 256)
|
||||
eveimageserver._eve_entity_image_url('corporation', 42, 256)
|
||||
)
|
||||
|
||||
def test_logo_url(self):
|
||||
@@ -621,4 +523,3 @@ class EveCorporationTestCase(TestCase):
|
||||
self.my_corp.logo_url_256,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=256'
|
||||
)
|
||||
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import os
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity
|
||||
from bravado.exception import HTTPNotFound
|
||||
from jsonschema.exceptions import RefResolutionError
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from . import set_logger
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..providers import ObjectNotFound, Entity, Character, Corporation, \
|
||||
Alliance, ItemType, EveProvider, EveSwaggerProvider
|
||||
from ..providers import (
|
||||
ObjectNotFound,
|
||||
Entity,
|
||||
Character,
|
||||
Corporation,
|
||||
Alliance,
|
||||
ItemType,
|
||||
EveProvider,
|
||||
EveSwaggerProvider
|
||||
)
|
||||
|
||||
|
||||
MODULE_PATH = 'allianceauth.eveonline.providers'
|
||||
SWAGGER_OLD_SPEC_PATH = os.path.join(os.path.dirname(
|
||||
os.path.abspath(__file__)), 'swagger_old.json'
|
||||
)
|
||||
os.path.abspath(__file__)), 'swagger_old.json'
|
||||
)
|
||||
set_logger(MODULE_PATH, __file__)
|
||||
|
||||
|
||||
@@ -51,7 +58,6 @@ class TestEntity(TestCase):
|
||||
x = Entity()
|
||||
self.assertEqual(repr(x), '<Entity (None): None>')
|
||||
|
||||
|
||||
def test_bool(self):
|
||||
x = Entity(1001)
|
||||
self.assertTrue(bool(x))
|
||||
@@ -99,7 +105,6 @@ class TestCorporation(TestCase):
|
||||
# should fetch alliance once only
|
||||
self.assertEqual(mock_provider_get_alliance.call_count, 1)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.EveSwaggerProvider.get_alliance')
|
||||
def test_alliance_not_defined(self, mock_provider_get_alliance):
|
||||
mock_provider_get_alliance.return_value = None
|
||||
@@ -110,7 +115,6 @@ class TestCorporation(TestCase):
|
||||
Entity(None, None)
|
||||
)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.EveSwaggerProvider.get_character')
|
||||
def test_ceo(self, mock_provider_get_character):
|
||||
my_ceo = Character(
|
||||
@@ -200,7 +204,6 @@ class TestAlliance(TestCase):
|
||||
# should be called once by used corp only
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 2)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.EveSwaggerProvider.get_corp')
|
||||
def test_corps(self, mock_provider_get_corp):
|
||||
mock_provider_get_corp.side_effect = TestAlliance._get_corp
|
||||
@@ -253,7 +256,6 @@ class TestCharacter(TestCase):
|
||||
|
||||
# should call the provider one time only
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 1)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.EveSwaggerProvider.get_alliance')
|
||||
@patch(MODULE_PATH + '.EveSwaggerProvider.get_corp')
|
||||
@@ -283,7 +285,6 @@ class TestCharacter(TestCase):
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 1)
|
||||
self.assertEqual(mock_provider_get_alliance.call_count, 1)
|
||||
|
||||
|
||||
def test_alliance_has_none(self):
|
||||
self.my_character.alliance_id = None
|
||||
self.assertEqual(self.my_character.alliance, Entity(None, None))
|
||||
@@ -343,7 +344,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_alliances_alliance_id_corporations(alliance_id):
|
||||
alliances = {
|
||||
@@ -357,7 +357,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_corporations_corporation_id(corporation_id):
|
||||
corporations = {
|
||||
@@ -382,7 +381,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_characters_character_id(character_id):
|
||||
characters = {
|
||||
@@ -403,7 +401,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_post_characters_affiliation(characters):
|
||||
character_data = {
|
||||
@@ -428,7 +425,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise TypeError()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_universe_types_type_id(type_id):
|
||||
types = {
|
||||
@@ -446,13 +442,11 @@ class TestEveSwaggerProvider(TestCase):
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_str(self, mock_esi_client_factory):
|
||||
my_provider = EveSwaggerProvider()
|
||||
self.assertEqual(str(my_provider), 'esi')
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_get_alliance(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
@@ -481,7 +475,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_alliance(3999)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_get_corp(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
@@ -508,7 +501,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_corp(2999)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_get_character(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
@@ -536,7 +528,6 @@ class TestEveSwaggerProvider(TestCase):
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_character(1999)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.esi_client_factory')
|
||||
def test_get_itemtype(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
@@ -601,5 +592,3 @@ class TestEveSwaggerProvider(TestCase):
|
||||
self.assertTrue(mock_esi_client_factory.called)
|
||||
self.assertIsNotNone(my_provider._client)
|
||||
self.assertEqual(my_client, 'my_client')
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@ from unittest.mock import patch, Mock
|
||||
from django.test import TestCase
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..tasks import update_alliance, update_corp, update_character, \
|
||||
from ..tasks import (
|
||||
update_alliance,
|
||||
update_corp,
|
||||
update_character,
|
||||
run_model_update
|
||||
)
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
@@ -13,98 +17,229 @@ class TestTasks(TestCase):
|
||||
def test_update_corp(self, mock_EveCorporationInfo):
|
||||
update_corp(42)
|
||||
self.assertEqual(
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_count,
|
||||
1
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_count, 1
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_args[0][0],
|
||||
42
|
||||
mock_EveCorporationInfo.objects.update_corporation.call_args[0][0], 42
|
||||
)
|
||||
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.EveAllianceInfo')
|
||||
def test_update_alliance(self, mock_EveAllianceInfo):
|
||||
update_alliance(42)
|
||||
self.assertEqual(
|
||||
mock_EveAllianceInfo.objects.update_alliance.call_args[0][0],
|
||||
42
|
||||
mock_EveAllianceInfo.objects.update_alliance.call_args[0][0], 42
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveAllianceInfo.objects\
|
||||
.update_alliance.return_value.populate_alliance.call_count,
|
||||
1
|
||||
mock_EveAllianceInfo.objects
|
||||
.update_alliance.return_value.populate_alliance.call_count, 1
|
||||
)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.EveCharacter')
|
||||
def test_update_character(self, mock_EveCharacter):
|
||||
update_character(42)
|
||||
self.assertEqual(
|
||||
mock_EveCharacter.objects.update_character.call_count,
|
||||
1
|
||||
mock_EveCharacter.objects.update_character.call_count, 1
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_EveCharacter.objects.update_character.call_args[0][0],
|
||||
42
|
||||
mock_EveCharacter.objects.update_character.call_args[0][0], 42
|
||||
)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.update_character')
|
||||
@patch('allianceauth.eveonline.tasks.update_alliance')
|
||||
@patch('allianceauth.eveonline.tasks.update_corp')
|
||||
def test_run_model_update(
|
||||
self,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
):
|
||||
|
||||
@patch('allianceauth.eveonline.tasks.update_character')
|
||||
@patch('allianceauth.eveonline.tasks.update_alliance')
|
||||
@patch('allianceauth.eveonline.tasks.update_corp')
|
||||
@patch('allianceauth.eveonline.providers.provider')
|
||||
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||
class TestRunModelUpdate(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
EveCorporationInfo.objects.all().delete()
|
||||
EveAllianceInfo.objects.all().delete()
|
||||
EveCharacter.objects.all().delete()
|
||||
|
||||
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id='2345',
|
||||
corporation_id=2345,
|
||||
corporation_name='corp.name',
|
||||
corporation_ticker='corp.ticker',
|
||||
corporation_ticker='c.c.t',
|
||||
member_count=10,
|
||||
alliance=None,
|
||||
)
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id='3456',
|
||||
alliance_id=3456,
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='alliance.ticker',
|
||||
executor_corp_id='alliance.executor_corp_id',
|
||||
)
|
||||
alliance_ticker='a.t',
|
||||
executor_corp_id=5,
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id='1234',
|
||||
character_name='character.name',
|
||||
corporation_id='character.corp.id',
|
||||
character_id=1,
|
||||
character_name='character.name1',
|
||||
corporation_id=2345,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id='character.alliance.id',
|
||||
alliance_id=None
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=2,
|
||||
character_name='character.name2',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=3,
|
||||
character_name='character.name3',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
EveCharacter.objects.create(
|
||||
character_id=4,
|
||||
character_name='character.name4',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
"""
|
||||
EveCharacter.objects.create(
|
||||
character_id=5,
|
||||
character_name='character.name5',
|
||||
corporation_id=9876,
|
||||
corporation_name='character.corp.name',
|
||||
corporation_ticker='c.c.t', # max 5 chars
|
||||
alliance_id=3456,
|
||||
alliance_name='character.alliance.name',
|
||||
)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.affiliations = [
|
||||
{'character_id': 1, 'corporation_id': 5},
|
||||
{'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456},
|
||||
{'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456},
|
||||
{'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456}
|
||||
]
|
||||
self.names = [
|
||||
{'id': 1, 'name': 'character.name1'},
|
||||
{'id': 2, 'name': 'character.name2'},
|
||||
{'id': 3, 'name': 'character.name3'},
|
||||
{'id': 4, 'name': 'character.name4_new'}
|
||||
]
|
||||
|
||||
def test_normal_run(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
run_model_update()
|
||||
|
||||
self.assertEqual(
|
||||
mock_provider.client.Character.post_characters_affiliation.call_count, 2
|
||||
)
|
||||
self.assertEqual(
|
||||
mock_provider.client.Universe.post_universe_names.call_count, 2
|
||||
)
|
||||
|
||||
# character 1 has changed corp
|
||||
# character 2 no change
|
||||
# character 3 has changed alliance
|
||||
# character 4 has changed name
|
||||
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
|
||||
self.assertEqual(
|
||||
int(mock_update_corp.apply_async.call_args[1]['args'][0]),
|
||||
2345
|
||||
int(mock_update_corp.apply_async.call_args[1]['args'][0]), 2345
|
||||
)
|
||||
|
||||
self.assertEqual(mock_update_alliance.apply_async.call_count, 1)
|
||||
self.assertEqual(
|
||||
int(mock_update_alliance.apply_async.call_args[1]['args'][0]),
|
||||
3456
|
||||
)
|
||||
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456
|
||||
)
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {1, 3, 4}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
|
||||
def test_ignore_character_not_in_affiliations(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
del self.affiliations[0]
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
self.assertEqual(mock_update_character.apply_async.call_count, 1)
|
||||
self.assertEqual(
|
||||
int(mock_update_character.apply_async.call_args[1]['args'][0]),
|
||||
1234
|
||||
)
|
||||
run_model_update()
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {3, 4}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
|
||||
def test_ignore_character_not_in_names(
|
||||
self,
|
||||
mock_provider,
|
||||
mock_update_corp,
|
||||
mock_update_alliance,
|
||||
mock_update_character,
|
||||
):
|
||||
def get_affiliations(characters: list):
|
||||
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
def get_names(ids: list):
|
||||
response = [x for x in self.names if x['id'] in ids]
|
||||
mock_operator = Mock(**{'result.return_value': response})
|
||||
return mock_operator
|
||||
|
||||
|
||||
del self.names[3]
|
||||
|
||||
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||
= get_affiliations
|
||||
|
||||
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||
|
||||
run_model_update()
|
||||
characters_updated = {
|
||||
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||
}
|
||||
excepted = {1, 3}
|
||||
self.assertSetEqual(characters_updated, excepted)
|
||||
|
||||
@@ -6,7 +6,7 @@ from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
|
||||
@hooks.register('menu_item_hook')
|
||||
def register_menu():
|
||||
return MenuItemHook(_('Fleet Activity Tracking'), 'fa fa-users fa-lightbulb-o fa-fw', 'fatlink:view',
|
||||
return MenuItemHook(_('Fleet Activity Tracking'), 'fas fa-users fa-fw', 'fatlink:view',
|
||||
navactive=['fatlink:'])
|
||||
|
||||
|
||||
|
||||
37
allianceauth/groupmanagement/auth_hooks.py
Normal file
37
allianceauth/groupmanagement/auth_hooks.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from allianceauth import hooks
|
||||
|
||||
from . import urls
|
||||
from .managers import GroupManager
|
||||
|
||||
|
||||
class GroupManagementMenuItem(MenuItemHook):
|
||||
""" This class ensures only authorized users will see the menu entry """
|
||||
def __init__(self):
|
||||
# setup menu entry for sidebar
|
||||
MenuItemHook.__init__(
|
||||
self,
|
||||
text=_('Group Management'),
|
||||
classes='fas fa-users-cog fa-fw',
|
||||
url_name='groupmanagement:management',
|
||||
order=50,
|
||||
navactive=['groupmanagement:management']
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
if GroupManager.can_manage_groups(request.user):
|
||||
self.count = GroupManager.pending_requests_count_for_user(request.user)
|
||||
return MenuItemHook.render(self, request)
|
||||
return ''
|
||||
|
||||
|
||||
@hooks.register('menu_item_hook')
|
||||
def register_menu():
|
||||
return GroupManagementMenuItem()
|
||||
|
||||
|
||||
@hooks.register('url_hook')
|
||||
def register_urls():
|
||||
return UrlHook(urls, 'group', r'^group/')
|
||||
@@ -1,5 +0,0 @@
|
||||
from allianceauth.groupmanagement.managers import GroupManager
|
||||
|
||||
|
||||
def can_manage_groups(request):
|
||||
return {'can_manage_groups': GroupManager.can_manage_groups(request.user)}
|
||||
@@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, User
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
from allianceauth.authentication.models import State
|
||||
from .models import GroupRequest
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -101,3 +102,18 @@ class GroupManager:
|
||||
if user.is_authenticated:
|
||||
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def pending_requests_count_for_user(cls, user: User) -> int:
|
||||
"""Returns the number of pending group requests for the given user"""
|
||||
|
||||
if cls.has_management_permission(user):
|
||||
return GroupRequest.objects.filter(status="pending").count()
|
||||
else:
|
||||
return (
|
||||
GroupRequest.objects
|
||||
.filter(status="pending")
|
||||
.filter(group__authgroup__group_leaders__exact=user)
|
||||
.select_related("group__authgroup__group_leaders")
|
||||
.count()
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from allianceauth.authentication.models import State
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class GroupRequest(models.Model):
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td class="text-center">{{ entry.date }}</td>
|
||||
<td class="text-center">{{ entry.date|date:"Y-M-d, H:i" }}</td>
|
||||
<td class="text-center">{{ entry.requestor }}</td>
|
||||
<td class="text-center">{{ entry.req_char }}</td>
|
||||
<td class="text-center">{{ entry.req_char.corporation_name }}</td>
|
||||
@@ -66,7 +66,8 @@
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
{% include 'bundles/moment-js.html' with locale=True %}
|
||||
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
@@ -74,7 +75,26 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
|
||||
$.fn.dataTable.moment = function ( format, locale ) {
|
||||
var types = $.fn.dataTable.ext.type;
|
||||
|
||||
// Add type detection
|
||||
types.detect.unshift( function ( d ) {
|
||||
return moment( d, format, locale, true ).isValid() ?
|
||||
'moment-'+format :
|
||||
null;
|
||||
} );
|
||||
|
||||
// Add sorting method - use an integer for the sorting
|
||||
types.order[ 'moment-'+format+'-pre' ] = function ( d ) {
|
||||
return moment( d, format, locale, true ).unix();
|
||||
};
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
$.fn.dataTable.moment( 'YYYY-MMM-D, HH:mm' );
|
||||
|
||||
$('#log-entries').DataTable({
|
||||
order: [[ 0, 'desc' ], [ 1, 'asc' ] ],
|
||||
filterDropDown:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td class="text-right">
|
||||
{% if member.is_leader %}
|
||||
<i class="fa fa-star"></i>
|
||||
<i class="fas fa-star"></i>
|
||||
{% endif %}
|
||||
<img src="{{ member.main_char|character_portrait_url:32 }}" class="img-circle">
|
||||
</td>
|
||||
@@ -69,7 +69,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-muted"><i class="fa fa-star"></i>: Group leader</p>
|
||||
<p class="text-muted"><i class="fas fa-star"></i>: Group leader</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block extra_javascript %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
|
||||
{% include 'bundles/clipboard-js.html' %}
|
||||
<script>
|
||||
new ClipboardJS('#clipboard-copy');
|
||||
</script>
|
||||
|
||||
@@ -20,8 +20,22 @@
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a data-toggle="tab" href="#add">{% trans "Join Requests" %}</a></li>
|
||||
<li><a data-toggle="tab" href="#leave">{% trans "Leave Requests" %}</a></li>
|
||||
<li class="active">
|
||||
<a data-toggle="tab" href="#add">
|
||||
{% trans "Join Requests" %}
|
||||
{% if acceptrequests %}
|
||||
<span class="badge">{{ acceptrequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-toggle="tab" href="#leave">
|
||||
{% trans "Leave Requests" %}
|
||||
{% if leaverequests %}
|
||||
<span class="badge">{{ leaverequests|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
@@ -11,11 +11,7 @@ from allianceauth.eveonline.models import (
|
||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
)
|
||||
|
||||
from ..admin import (
|
||||
HasLeaderFilter,
|
||||
GroupAdmin,
|
||||
Group
|
||||
)
|
||||
from ..admin import HasLeaderFilter, GroupAdmin, Group
|
||||
from . import get_admin_change_view_url
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
@@ -88,33 +84,33 @@ class TestGroupAdmin(TestCase):
|
||||
|
||||
# user 1 - corp and alliance, normal user
|
||||
cls.character_1 = EveCharacter.objects.create(
|
||||
character_id='1001',
|
||||
character_id=1001,
|
||||
character_name='Bruce Wayne',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
cls.character_1a = EveCharacter.objects.create(
|
||||
character_id='1002',
|
||||
character_id=1002,
|
||||
character_name='Batman',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
executor_corp_id='2001'
|
||||
executor_corp_id=2001
|
||||
)
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
member_count=42,
|
||||
@@ -189,10 +185,10 @@ class TestGroupAdmin(TestCase):
|
||||
alliance=None
|
||||
)
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id='3101',
|
||||
alliance_id=3101,
|
||||
alliance_name='Lex World Domination',
|
||||
alliance_ticker='LWD',
|
||||
executor_corp_id=''
|
||||
executor_corp_id=2101
|
||||
)
|
||||
cls.user_3 = User.objects.create_user(
|
||||
cls.character_3.character_name.replace(' ', '_'),
|
||||
@@ -219,8 +215,8 @@ class TestGroupAdmin(TestCase):
|
||||
"""create autogroups for corps and alliances"""
|
||||
if _has_auto_groups:
|
||||
autogroups_config = AutogroupsConfig(
|
||||
corp_groups = True,
|
||||
alliance_groups = True
|
||||
corp_groups=True,
|
||||
alliance_groups=True
|
||||
)
|
||||
autogroups_config.save()
|
||||
for state in State.objects.all():
|
||||
@@ -277,7 +273,7 @@ class TestGroupAdmin(TestCase):
|
||||
|
||||
if _has_auto_groups:
|
||||
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||
def test_properties_6(self):
|
||||
def test_properties_7(self):
|
||||
self._create_autogroups()
|
||||
expected = ['Auto Group']
|
||||
my_group = Group.objects\
|
||||
@@ -337,8 +333,8 @@ class TestGroupAdmin(TestCase):
|
||||
changelist = my_modeladmin.get_changelist_instance(request)
|
||||
queryset = changelist.get_queryset(request)
|
||||
expected = Group.objects.exclude(
|
||||
managedalliancegroup__isnull=True,
|
||||
managedcorpgroup__isnull=True
|
||||
managedalliancegroup__isnull=True,
|
||||
managedcorpgroup__isnull=True
|
||||
)
|
||||
self.assertSetEqual(set(queryset), set(expected))
|
||||
|
||||
@@ -394,4 +390,4 @@ class TestGroupAdmin(TestCase):
|
||||
c = Client()
|
||||
c.login(username='superuser', password='secret')
|
||||
response = c.get(get_admin_change_view_url(self.group_1))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.urls import reverse
|
||||
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..models import AuthGroup
|
||||
from ..models import GroupRequest
|
||||
from ..managers import GroupManager
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class MockUserNotAuthenticated():
|
||||
def __init__(self):
|
||||
self.is_authenticated = False
|
||||
|
||||
|
||||
class GroupManagementVisibilityTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -37,22 +38,20 @@ class GroupManagementVisibilityTestCase(TestCase):
|
||||
def _refresh_user(self):
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
|
||||
|
||||
def test_get_group_leaders_groups(self):
|
||||
self.group1.authgroup.group_leaders.add(self.user)
|
||||
self.group2.authgroup.group_leader_groups.add(self.group1)
|
||||
self._refresh_user()
|
||||
groups = GroupManager.get_group_leaders_groups(self.user)
|
||||
|
||||
self.assertIn(self.group1, groups) #avail due to user
|
||||
self.assertNotIn(self.group2, groups) #not avail due to group
|
||||
self.assertNotIn(self.group3, groups) #not avail at all
|
||||
self.assertIn(self.group1, groups) #avail due to user
|
||||
self.assertNotIn(self.group2, groups) #not avail due to group
|
||||
self.assertNotIn(self.group3, groups) #not avail at all
|
||||
|
||||
self.user.groups.add(self.group1)
|
||||
self._refresh_user()
|
||||
groups = GroupManager.get_group_leaders_groups(self.user)
|
||||
|
||||
|
||||
def test_can_manage_group(self):
|
||||
self.group1.authgroup.group_leaders.add(self.user)
|
||||
self.user.groups.add(self.group1)
|
||||
@@ -182,7 +181,6 @@ class TestGroupManager(TestCase):
|
||||
]:
|
||||
self.assertFalse(GroupManager.joinable_group(x, member_state))
|
||||
|
||||
|
||||
def test_joinable_group_guest(self):
|
||||
guest_state = AuthUtils.get_guest_state()
|
||||
for x in [
|
||||
@@ -200,7 +198,6 @@ class TestGroupManager(TestCase):
|
||||
]:
|
||||
self.assertFalse(GroupManager.joinable_group(x, guest_state))
|
||||
|
||||
|
||||
def test_get_all_non_internal_groups(self):
|
||||
result = GroupManager.get_all_non_internal_groups()
|
||||
expected = {
|
||||
@@ -224,7 +221,7 @@ class TestGroupManager(TestCase):
|
||||
def test_get_joinable_groups_for_user_no_permission(self):
|
||||
AuthUtils.assign_state(self.user, AuthUtils.get_guest_state())
|
||||
result = GroupManager.get_joinable_groups_for_user(self.user)
|
||||
expected= {self.group_public_1, self.group_public_2}
|
||||
expected = {self.group_public_1, self.group_public_2}
|
||||
self.assertSetEqual(set(result), expected)
|
||||
|
||||
def test_get_joinable_groups_for_user_guest_w_permission_(self):
|
||||
@@ -335,3 +332,96 @@ class TestGroupManager(TestCase):
|
||||
self.assertFalse(
|
||||
GroupManager.can_manage_group(user, self.group_default)
|
||||
)
|
||||
|
||||
|
||||
class TestPendingRequestsCountForUser(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.group_1 = Group.objects.create(name="Group 1")
|
||||
self.group_2 = Group.objects.create(name="Group 2")
|
||||
self.user_leader_1 = AuthUtils.create_member('Clark Kent')
|
||||
self.group_1.authgroup.group_leaders.add(self.user_leader_1)
|
||||
self.user_leader_2 = AuthUtils.create_member('Peter Parker')
|
||||
self.group_2.authgroup.group_leaders.add(self.user_leader_2)
|
||||
self.user_requestor = AuthUtils.create_member('Bruce Wayne')
|
||||
|
||||
def test_single_request_for_leader(self):
|
||||
# given user_leader_1 is leader of group_1
|
||||
# and user_leader_2 is leader of group_2
|
||||
# when user_requestor is requesting access to group 1
|
||||
# then return 1 for user_leader 1 and 0 for user_leader_2
|
||||
GroupRequest.objects.create(
|
||||
status="pending", user=self.user_requestor, group=self.group_1
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_leader_2), 0
|
||||
)
|
||||
|
||||
def test_return_none_for_none_leader(self):
|
||||
# given user_requestor is leader of no group
|
||||
# when user_requestor is requesting access to group 1
|
||||
# then return 0 for user_requestor
|
||||
GroupRequest.objects.create(
|
||||
status="pending", user=self.user_requestor, group=self.group_1
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_requestor), 0
|
||||
)
|
||||
|
||||
def test_single_leave_request(self):
|
||||
# given user_leader_2 is leader of group_2
|
||||
# and user_requestor is member of group 2
|
||||
# when user_requestor is requesting to leave group 2
|
||||
# then return 1 for user_leader_2
|
||||
self.user_requestor.groups.add(self.group_2)
|
||||
|
||||
GroupRequest.objects.create(
|
||||
status="pending",
|
||||
user=self.user_requestor,
|
||||
group=self.group_2,
|
||||
leave_request=True
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_leader_2), 1
|
||||
)
|
||||
|
||||
def test_join_and_leave_request(self):
|
||||
# given user_leader_2 is leader of group_2
|
||||
# and user_requestor is member of group 2
|
||||
# when user_requestor is requesting to leave group 2
|
||||
# and user_requestor_2 is requesting to join group 2
|
||||
# then return 2 for user_leader_2
|
||||
self.user_requestor.groups.add(self.group_2)
|
||||
user_requestor_2 = AuthUtils.create_member("Lex Luther")
|
||||
GroupRequest.objects.create(
|
||||
status="pending",
|
||||
user=user_requestor_2,
|
||||
group=self.group_2
|
||||
)
|
||||
GroupRequest.objects.create(
|
||||
status="pending",
|
||||
user=self.user_requestor,
|
||||
group=self.group_2,
|
||||
leave_request=True
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_leader_2), 2
|
||||
)
|
||||
|
||||
def test_single_request_for_user_with_management_perm(self):
|
||||
# given user_leader_4 which is leafer of no group
|
||||
# but has the management permissions
|
||||
# when user_requestor is requesting access to group 1
|
||||
# then return 1 for user_leader_4
|
||||
user_leader_4 = AuthUtils.create_member("Lex Luther")
|
||||
AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4)
|
||||
user_leader_4 = User.objects.get(pk=user_leader_4.pk)
|
||||
GroupRequest.objects.create(
|
||||
status="pending", user=self.user_requestor, group=self.group_1
|
||||
)
|
||||
self.assertEqual(
|
||||
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
|
||||
)
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
from . import views
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls import url
|
||||
app_name = 'groupmanagement'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^groups/', views.groups_view, name='groups'),
|
||||
url(r'^group/', include([
|
||||
url(r'^management/', views.group_management,
|
||||
name='management'),
|
||||
url(r'^membership/$', views.group_membership,
|
||||
name='membership'),
|
||||
url(r'^membership/(\w+)/$', views.group_membership_list,
|
||||
name='membership_list'),
|
||||
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
|
||||
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
|
||||
name='membership_remove'),
|
||||
url(r'^request_add/(\w+)', views.group_request_add,
|
||||
name='request_add'),
|
||||
url(r'^request/accept/(\w+)', views.group_accept_request,
|
||||
name='accept_request'),
|
||||
url(r'^request/reject/(\w+)', views.group_reject_request,
|
||||
name='reject_request'),
|
||||
|
||||
url(r'^request_leave/(\w+)', views.group_request_leave,
|
||||
name='request_leave'),
|
||||
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
|
||||
name='leave_accept_request'),
|
||||
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
|
||||
name='leave_reject_request'),
|
||||
])),
|
||||
url(r'^groups/', views.groups_view, name='groups'),
|
||||
url(r'^management/', views.group_management,
|
||||
name='management'),
|
||||
url(r'^membership/$', views.group_membership,
|
||||
name='membership'),
|
||||
url(r'^membership/(\w+)/$', views.group_membership_list,
|
||||
name='membership_list'),
|
||||
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
|
||||
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
|
||||
name='membership_remove'),
|
||||
url(r'^request_add/(\w+)', views.group_request_add,
|
||||
name='request_add'),
|
||||
url(r'^request/accept/(\w+)', views.group_accept_request,
|
||||
name='accept_request'),
|
||||
url(r'^request/reject/(\w+)', views.group_reject_request,
|
||||
name='reject_request'),
|
||||
url(r'^request_leave/(\w+)', views.group_request_leave,
|
||||
name='request_leave'),
|
||||
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
|
||||
name='leave_accept_request'),
|
||||
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
|
||||
name='leave_reject_request'),
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
from django.db.models import Count
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
@@ -28,7 +27,7 @@ def group_management(request):
|
||||
acceptrequests = []
|
||||
leaverequests = []
|
||||
|
||||
base_group_query = GroupRequest.objects.select_related('user', 'group')
|
||||
base_group_query = GroupRequest.objects.select_related('user', 'group', 'user__profile__main_character')
|
||||
if GroupManager.has_management_permission(request.user):
|
||||
# Full access
|
||||
group_requests = base_group_query.all()
|
||||
@@ -76,7 +75,6 @@ def group_membership_audit(request, group_id):
|
||||
logger.debug("group_management_audit called by user %s" % request.user)
|
||||
group = get_object_or_404(Group, id=group_id)
|
||||
try:
|
||||
|
||||
# Check its a joinable group i.e. not corp or internal
|
||||
# And the user has permission to manage it
|
||||
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
@@ -93,8 +91,6 @@ def group_membership_audit(request, group_id):
|
||||
return render(request, 'groupmanagement/audit.html', context=render_items)
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(GroupManager.can_manage_groups)
|
||||
def group_membership_list(request, group_id):
|
||||
@@ -124,7 +120,7 @@ def group_membership_list(request, group_id):
|
||||
for member in \
|
||||
group.user_set\
|
||||
.all()\
|
||||
.select_related('profile')\
|
||||
.select_related('profile', 'profile__main_character')\
|
||||
.order_by('profile__main_character__character_name'):
|
||||
|
||||
members.append({
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from allianceauth import hooks
|
||||
from allianceauth.hrapplications import urls
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
|
||||
from . import urls
|
||||
from .models import Application
|
||||
|
||||
|
||||
class ApplicationsMenu(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
_('Applications'),
|
||||
'fa fa-file-o fa-fw',
|
||||
'far fa-file fa-fw',
|
||||
'hrapplications:index',
|
||||
navactive=['hrapplications:'])
|
||||
|
||||
def render(self, request):
|
||||
app_count = Application.objects.pending_requests_count_for_user(request.user)
|
||||
self.count = app_count if app_count and app_count > 0 else None
|
||||
return MenuItemHook.render(self, request)
|
||||
|
||||
|
||||
@hooks.register('menu_item_hook')
|
||||
def register_menu():
|
||||
|
||||
25
allianceauth/hrapplications/managers.py
Normal file
25
allianceauth/hrapplications/managers.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ApplicationManager(models.Manager):
|
||||
|
||||
def pending_requests_count_for_user(self, user: User) -> Optional[int]:
|
||||
"""Returns the number of pending group requests for the given user"""
|
||||
if user.is_superuser:
|
||||
return self.filter(approved__isnull=True).count()
|
||||
elif user.has_perm("auth.human_resources"):
|
||||
main_character = user.profile.main_character
|
||||
if main_character:
|
||||
return (
|
||||
self
|
||||
.select_related("form__corp")
|
||||
.filter(form__corp__corporation_id=main_character.corporation_id)
|
||||
.filter(approved__isnull=True)
|
||||
.count()
|
||||
)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
@@ -2,8 +2,9 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.eveonline.models import EveCorporationInfo
|
||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
|
||||
|
||||
from .managers import ApplicationManager
|
||||
|
||||
|
||||
class ApplicationQuestion(models.Model):
|
||||
@@ -22,6 +23,7 @@ class ApplicationChoice(models.Model):
|
||||
def __str__(self):
|
||||
return self.choice_text
|
||||
|
||||
|
||||
class ApplicationForm(models.Model):
|
||||
questions = SortedManyToManyField(ApplicationQuestion)
|
||||
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
||||
@@ -38,6 +40,8 @@ class Application(models.Model):
|
||||
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
objects = ApplicationManager()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user) + " Application To " + str(self.form)
|
||||
|
||||
|
||||
@@ -1 +1,103 @@
|
||||
# Create your tests here.
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.eveonline.models import EveCorporationInfo
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from .models import Application, ApplicationForm, ApplicationQuestion, ApplicationChoice
|
||||
|
||||
|
||||
class TestApplicationManagersPendingRequestsCountForUser(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.corporation_1 = EveCorporationInfo.objects.create(
|
||||
corporation_id=2001, corporation_name="Wayne Tech", member_count=42
|
||||
)
|
||||
self.corporation_2 = EveCorporationInfo.objects.create(
|
||||
corporation_id=2011, corporation_name="Lex Corp", member_count=666
|
||||
)
|
||||
question = ApplicationQuestion.objects.create(title="Dummy Question")
|
||||
ApplicationChoice.objects.create(question=question, choice_text="yes")
|
||||
ApplicationChoice.objects.create(question=question, choice_text="no")
|
||||
self.form_corporation_1 = ApplicationForm.objects.create(
|
||||
corp=self.corporation_1
|
||||
)
|
||||
self.form_corporation_1.questions.add(question)
|
||||
self.form_corporation_2 = ApplicationForm.objects.create(
|
||||
corp=self.corporation_2
|
||||
)
|
||||
self.form_corporation_2.questions.add(question)
|
||||
|
||||
self.user_requestor = AuthUtils.create_member("Peter Parker")
|
||||
|
||||
self.user_manager = AuthUtils.create_member("Bruce Wayne")
|
||||
AuthUtils.add_main_character_2(
|
||||
self.user_manager,
|
||||
self.user_manager.username,
|
||||
1001,
|
||||
self.corporation_1.corporation_id,
|
||||
self.corporation_1.corporation_name,
|
||||
)
|
||||
AuthUtils.add_permission_to_user_by_name(
|
||||
"auth.human_resources", self.user_manager
|
||||
)
|
||||
self.user_manager = User.objects.get(pk=self.user_manager.pk)
|
||||
|
||||
def test_no_pending_application(self):
|
||||
# given manager of corporation 1 has permission
|
||||
# when no application is pending for corporation 1
|
||||
# return 0
|
||||
self.assertEqual(
|
||||
Application.objects.pending_requests_count_for_user(self.user_manager), 0
|
||||
)
|
||||
|
||||
def test_single_pending_application(self):
|
||||
# given manager of corporation 1 has permission
|
||||
# when 1 application is pending for corporation 1
|
||||
# return 1
|
||||
Application.objects.create(
|
||||
form=self.form_corporation_1, user=self.user_requestor
|
||||
)
|
||||
self.assertEqual(
|
||||
Application.objects.pending_requests_count_for_user(self.user_manager), 1
|
||||
)
|
||||
|
||||
def test_user_has_no_permission(self):
|
||||
# given user has no permission
|
||||
# when 1 application is pending
|
||||
# return None
|
||||
self.assertIsNone(
|
||||
Application.objects.pending_requests_count_for_user(self.user_requestor)
|
||||
)
|
||||
|
||||
def test_two_pending_applications_for_different_corporations_normal_manager(self):
|
||||
# given manager of corporation 1 has permission
|
||||
# when 1 application is pending for corporation 1
|
||||
# and 1 application is pending for corporation 2
|
||||
# return 1
|
||||
Application.objects.create(
|
||||
form=self.form_corporation_1, user=self.user_requestor
|
||||
)
|
||||
Application.objects.create(
|
||||
form=self.form_corporation_2, user=self.user_requestor
|
||||
)
|
||||
self.assertEqual(
|
||||
Application.objects.pending_requests_count_for_user(self.user_manager), 1
|
||||
)
|
||||
|
||||
def test_two_pending_applications_for_different_corporations_manager_is_super(self):
|
||||
# given manager of corporation 1 has permission
|
||||
# when 1 application is pending for corporation 1
|
||||
# and 1 application is pending for corporation 2
|
||||
# return 1
|
||||
Application.objects.create(
|
||||
form=self.form_corporation_1, user=self.user_requestor
|
||||
)
|
||||
Application.objects.create(
|
||||
form=self.form_corporation_2, user=self.user_requestor
|
||||
)
|
||||
superuser = User.objects.create_superuser(
|
||||
"Superman", "superman@example.com", "password"
|
||||
)
|
||||
self.assertEqual(
|
||||
Application.objects.pending_requests_count_for_user(superuser), 2
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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 "暗色皮肤"
|
||||
|
||||
@@ -7,7 +7,7 @@ from . import urls
|
||||
class OpTimerboardMenu(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self, _('Fleet Operations'),
|
||||
'fa fa-exclamation fa-fw',
|
||||
'fas fa-exclamation fa-fw',
|
||||
'optimer:view',
|
||||
navactive=['optimer:'])
|
||||
|
||||
|
||||
@@ -36,9 +36,15 @@
|
||||
{% block extra_script %}
|
||||
|
||||
$('#id_start').datetimepicker({
|
||||
lang: '{{ LANGUAGE_CODE }}',
|
||||
maskInput: true,
|
||||
format: 'Y-m-d H:i',minDate:0
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
{% if NIGHT_MODE %}
|
||||
theme: 'dark',
|
||||
{% else %}
|
||||
theme: 'default',
|
||||
{% endif %}
|
||||
mask: true,
|
||||
format: 'Y-m-d H:i',
|
||||
minDate: 0
|
||||
});
|
||||
|
||||
{% endblock extra_script %}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
{% include 'bundles/moment-js.html' with locale=True %}
|
||||
<script src="{% static 'js/timers.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
<script type="application/javascript">
|
||||
// Data
|
||||
var timers = [
|
||||
{% for op in optimer %}
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endfor %}
|
||||
];
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
<script type="application/javascript">
|
||||
|
||||
timedUpdate();
|
||||
setAllLocalTimes();
|
||||
|
||||
@@ -44,9 +44,15 @@
|
||||
{% block extra_script %}
|
||||
|
||||
$('#id_start').datetimepicker({
|
||||
lang: '{{ LANGUAGE_CODE }}',
|
||||
maskInput: true,
|
||||
format: 'Y-m-d H:i',minDate:0
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
{% if NIGHT_MODE %}
|
||||
theme: 'dark',
|
||||
{% else %}
|
||||
theme: 'default',
|
||||
{% endif %}
|
||||
mask: true,
|
||||
format: 'Y-m-d H:i',
|
||||
minDate: 0
|
||||
});
|
||||
|
||||
{% endblock extra_script %}
|
||||
|
||||
@@ -8,7 +8,7 @@ class PermissionsTool(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
'Permissions Audit',
|
||||
'fa fa-key fa-id-card',
|
||||
'fas fa-id-card fa-fw',
|
||||
'permissions_tool:overview',
|
||||
order=400,
|
||||
navactive=['permissions_tool:'])
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
|
||||
@@ -41,7 +41,7 @@ class PermissionsToolViewsTestCase(WebTest):
|
||||
response_content = response.content.decode('utf-8')
|
||||
|
||||
self.assertInHTML('<li><a class="active" href="/permissions/overview/">'
|
||||
'<i class="fa fa-key fa-id-card"></i> Permissions Audit</a></li>', response_content)
|
||||
'<i class="fas fa-id-card fa-fw"></i> Permissions Audit</a></li>', response_content)
|
||||
|
||||
def test_permissions_overview(self):
|
||||
self.app.set_user(self.member)
|
||||
|
||||
@@ -103,8 +103,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.tz',
|
||||
'allianceauth.notifications.context_processors.user_notification_count',
|
||||
'allianceauth.groupmanagement.context_processors.can_manage_groups',
|
||||
'allianceauth.notifications.context_processors.user_notification_count',
|
||||
'allianceauth.context_processors.auth_settings',
|
||||
],
|
||||
},
|
||||
@@ -221,7 +220,7 @@ LOGGING = {
|
||||
'backupCount': 5, # edit this line to change number of log backups
|
||||
},
|
||||
'extension_file': {
|
||||
'level': 'DEBUG',
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(BASE_DIR, 'log/extensions.log'),
|
||||
'formatter': 'verbose',
|
||||
|
||||
@@ -22,6 +22,10 @@ INSTALLED_APPS += [
|
||||
|
||||
]
|
||||
|
||||
# To change the logging level for extensions, uncomment the following line.
|
||||
# LOGGING['handlers']['extension_file']['level'] = 'DEBUG'
|
||||
|
||||
|
||||
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
|
||||
DATABASES['default'] = {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
|
||||
@@ -9,7 +9,7 @@ class Services(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
_('Services'),
|
||||
'fa fa-cogs fa-fw',
|
||||
'fas fa-cogs fa-fw',
|
||||
'services:services', 100)
|
||||
|
||||
def render(self, request):
|
||||
|
||||
@@ -14,20 +14,22 @@ def get_extension_logger(name):
|
||||
Takes the name of a plugin/extension and generates a child logger of the extensions logger
|
||||
to be used by the extension to log events to the extensions logger.
|
||||
|
||||
The logging level is decided by whether or not DEBUG is set to true in the project settings. If
|
||||
DEBUG is set to false, then the logging level is set to INFO.
|
||||
The logging level is determined by the level defined for the parent logger.
|
||||
|
||||
:param: name: the name of the extension doing the logging
|
||||
:return: an extensions child logger
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
raise TypeError(f"get_extension_logger takes an argument of type string."
|
||||
f"Instead received argument of type {type(name).__name__}.")
|
||||
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
parent_logger = logging.getLogger('extensions')
|
||||
|
||||
logger = logging.getLogger('extensions.' + name)
|
||||
logger.name = name
|
||||
logger.level = logging.INFO
|
||||
if settings.DEBUG:
|
||||
logger.level = logging.DEBUG
|
||||
logger.level = parent_logger.level
|
||||
|
||||
return logger
|
||||
|
||||
@@ -137,6 +139,11 @@ class MenuItemHook:
|
||||
self.url_name = url_name
|
||||
self.template = 'public/menuitem.html'
|
||||
self.order = order if order is not None else 9999
|
||||
|
||||
# count is an integer shown next to the menu item as badge when count != None
|
||||
# apps need to set the count in their child class, e.g. in render() method
|
||||
self.count = None
|
||||
|
||||
navactive = navactive or []
|
||||
navactive.append(url_name)
|
||||
self.navactive = navactive
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,33 +3,38 @@ from ..utils import clean_setting
|
||||
|
||||
# Base URL for all API calls. Must end with /.
|
||||
DISCORD_API_BASE_URL = clean_setting(
|
||||
'DISCORD_API_BASE_URL', 'https://discordapp.com/api/'
|
||||
'DISCORD_API_BASE_URL', 'https://discord.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
|
||||
DISCORD_OAUTH_BASE_URL = clean_setting(
|
||||
'DISCORD_OAUTH_BASE_URL', 'https://discordapp.com/api/oauth2/authorize'
|
||||
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
|
||||
)
|
||||
|
||||
# Base authorization URL for Discord Oauth
|
||||
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
||||
'DISCORD_OAUTH_TOKEN_URL', 'https://discordapp.com/api/oauth2/token'
|
||||
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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,
|
||||
@@ -46,6 +47,9 @@ DURATION_CONTINGENCY = 500
|
||||
# time until next reset is below this threshold
|
||||
WAIT_THRESHOLD = 250
|
||||
|
||||
# Minimum wait duration when doing a blocking wait
|
||||
MINIMUM_BLOCKING_WAIT = 50
|
||||
|
||||
# If the rate limit resets soon we will wait it out and then retry to
|
||||
# either get a remaining request from our cached counter
|
||||
# or again wait out a short reset time and retry again.
|
||||
@@ -176,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:
|
||||
@@ -189,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 = ''
|
||||
@@ -226,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
|
||||
|
||||
@@ -270,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
|
||||
|
||||
@@ -277,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
|
||||
@@ -307,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
|
||||
@@ -537,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
|
||||
@@ -604,8 +622,11 @@ class DiscordClient:
|
||||
name=self._KEY_GLOBAL_RATE_LIMIT_REMAINING,
|
||||
value=RATE_LIMIT_MAX_REQUESTS,
|
||||
px=RATE_LIMIT_RESETS_AFTER + DURATION_CONTINGENCY
|
||||
)
|
||||
resets_in = self._redis.pttl(self._KEY_GLOBAL_RATE_LIMIT_REMAINING)
|
||||
)
|
||||
resets_in = max(
|
||||
MINIMUM_BLOCKING_WAIT,
|
||||
self._redis.pttl(self._KEY_GLOBAL_RATE_LIMIT_REMAINING)
|
||||
)
|
||||
if requests_remaining >= 0:
|
||||
logger.debug(
|
||||
'%s: Got one of %d remaining requests until reset in %s ms',
|
||||
@@ -615,7 +636,7 @@ class DiscordClient:
|
||||
)
|
||||
return requests_remaining
|
||||
|
||||
elif resets_in < WAIT_THRESHOLD:
|
||||
elif resets_in < WAIT_THRESHOLD:
|
||||
sleep(resets_in / 1000)
|
||||
logger.debug(
|
||||
'%s: No requests remaining until reset in %d ms. '
|
||||
|
||||
@@ -33,7 +33,7 @@ logger = set_logger_to_file(
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
|
||||
API_BASE_URL = 'https://discordapp.com/api/'
|
||||
API_BASE_URL = 'https://discord.com/api/'
|
||||
|
||||
TEST_RETRY_AFTER = 3000
|
||||
|
||||
@@ -50,6 +50,12 @@ mock_redis = MagicMock(**{
|
||||
})
|
||||
|
||||
|
||||
# default mock function to simulate sleep
|
||||
def my_sleep(value):
|
||||
if value < 0:
|
||||
raise ValueError('sleep length must be non-negative')
|
||||
|
||||
|
||||
class DiscordClient2(DiscordClient):
|
||||
"""Variant that overwrites lua wrappers with dummies for easier testing"""
|
||||
|
||||
@@ -274,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(
|
||||
@@ -285,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')
|
||||
@@ -296,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)
|
||||
|
||||
|
||||
@@ -838,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,
|
||||
@@ -890,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,
|
||||
@@ -1034,6 +1093,42 @@ class TestRateLimitMechanic(TestCase):
|
||||
requests_mocker.post(
|
||||
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles', json=self.my_role
|
||||
)
|
||||
mock_sleep.side_effect = my_sleep
|
||||
my_mock_redis = MagicMock(**{'pttl.side_effect': my_redis_pttl_2})
|
||||
mock_redis_decr_or_set.side_effect = my_redis_decr_or_set
|
||||
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
|
||||
|
||||
result = client.create_guild_role(
|
||||
guild_id=TEST_GUILD_ID, role_name=self.my_role['name']
|
||||
)
|
||||
self.assertDictEqual(result, self.my_role)
|
||||
self.assertTrue(mock_sleep.called)
|
||||
|
||||
@patch(MODULE_PATH + '.sleep')
|
||||
def test_wait_if_reset_happens_soon_and_sleep_must_not_be_negative(
|
||||
self, requests_mocker, mock_sleep, mock_redis_decr_or_set
|
||||
):
|
||||
counter = 0
|
||||
|
||||
def my_redis_pttl_2(name: str):
|
||||
if name == DiscordClient._KEY_GLOBAL_BACKOFF_UNTIL:
|
||||
return -1
|
||||
else:
|
||||
return -1
|
||||
|
||||
def my_redis_decr_or_set(**kwargs):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
|
||||
if counter < 2:
|
||||
return -1
|
||||
else:
|
||||
return 5
|
||||
|
||||
requests_mocker.post(
|
||||
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles', json=self.my_role
|
||||
)
|
||||
mock_sleep.side_effect = my_sleep
|
||||
my_mock_redis = MagicMock(**{'pttl.side_effect': my_redis_pttl_2})
|
||||
mock_redis_decr_or_set.side_effect = my_redis_decr_or_set
|
||||
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
|
||||
@@ -1075,6 +1170,7 @@ class TestRateLimitMechanic(TestCase):
|
||||
requests_mocker.post(
|
||||
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles', json=self.my_role
|
||||
)
|
||||
mock_sleep.side_effect = my_sleep
|
||||
my_mock_redis = MagicMock(**{'pttl.side_effect': my_redis_pttl_2})
|
||||
mock_redis_decr_or_set.return_value = -1
|
||||
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
|
||||
@@ -1208,7 +1304,8 @@ class TestBackoffHandling(TestCase):
|
||||
requests_mocker.post(
|
||||
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles', json=self.my_role
|
||||
)
|
||||
retry_after = 50
|
||||
retry_after = 50
|
||||
mock_sleep.side_effect = my_sleep
|
||||
my_mock_redis = MagicMock(**{'pttl.return_value': retry_after})
|
||||
mock_redis_decr_or_set.return_value = 5
|
||||
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..discord_client.tests import ( # noqa
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
ROLE_MIKE,
|
||||
ALL_ROLES,
|
||||
create_user_info
|
||||
)
|
||||
|
||||
|
||||
@@ -34,33 +34,33 @@ class TestDataMixin(TestCase):
|
||||
|
||||
# user 1 - corp and alliance, normal user
|
||||
cls.character_1 = EveCharacter.objects.create(
|
||||
character_id='1001',
|
||||
character_id=1001,
|
||||
character_name='Bruce Wayne',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
cls.character_1a = EveCharacter.objects.create(
|
||||
character_id='1002',
|
||||
character_id=1002,
|
||||
character_name='Batman',
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
)
|
||||
alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id='3001',
|
||||
alliance_id=3001,
|
||||
alliance_name='Wayne Enterprises',
|
||||
alliance_ticker='WE',
|
||||
executor_corp_id='2001'
|
||||
executor_corp_id=2001
|
||||
)
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id='2001',
|
||||
corporation_id=2001,
|
||||
corporation_name='Wayne Technologies',
|
||||
corporation_ticker='WT',
|
||||
member_count=42,
|
||||
@@ -141,10 +141,10 @@ class TestDataMixin(TestCase):
|
||||
alliance=None
|
||||
)
|
||||
EveAllianceInfo.objects.create(
|
||||
alliance_id='3101',
|
||||
alliance_id=3101,
|
||||
alliance_name='Lex World Domination',
|
||||
alliance_ticker='LWD',
|
||||
executor_corp_id=''
|
||||
executor_corp_id=2101
|
||||
)
|
||||
cls.user_3 = User.objects.create_user(
|
||||
cls.character_3.character_name.replace(' ', '_'),
|
||||
@@ -245,8 +245,8 @@ class TestFilters(TestDataMixin, TestCase):
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
('2002', 'Daily Planet'),
|
||||
('2001', 'Wayne Technologies'),
|
||||
(2002, 'Daily Planet'),
|
||||
(2001, 'Wayne Technologies'),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
@@ -274,7 +274,7 @@ class TestFilters(TestDataMixin, TestCase):
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
('3001', 'Wayne Enterprises'),
|
||||
(3001, 'Wayne Enterprises'),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from hashlib import md5
|
||||
|
||||
from . import providers
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
|
||||
@@ -19,128 +19,8 @@ class DiscourseError(Exception):
|
||||
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
||||
|
||||
|
||||
# not exhaustive, only the ones we need
|
||||
ENDPOINTS = {
|
||||
'groups': {
|
||||
'list': {
|
||||
'path': "/groups/search.json",
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'create': {
|
||||
'path': "/admin/groups",
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': ['name'],
|
||||
'optional': ['visible'],
|
||||
}
|
||||
},
|
||||
'add_user': {
|
||||
'path': "/admin/groups/%s/members.json",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['usernames'],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'remove_user': {
|
||||
'path': "/admin/groups/%s/members.json",
|
||||
'method': 'delete',
|
||||
'args': {
|
||||
'required': ['username'],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'delete': {
|
||||
'path': "/admin/groups/%s.json",
|
||||
'method': 'delete',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'users': {
|
||||
'create': {
|
||||
'path': "/users",
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': ['name', 'email', 'password', 'username'],
|
||||
'optional': ['active'],
|
||||
},
|
||||
},
|
||||
'update': {
|
||||
'path': "/users/%s.json",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['params'],
|
||||
'optional': [],
|
||||
}
|
||||
},
|
||||
'get': {
|
||||
'path': "/users/%s.json",
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'activate': {
|
||||
'path': "/admin/users/%s/activate",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'set_email': {
|
||||
'path': "/users/%s/preferences/email",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['email'],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'suspend': {
|
||||
'path': "/admin/users/%s/suspend",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['duration', 'reason'],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'unsuspend': {
|
||||
'path': "/admin/users/%s/unsuspend",
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'logout': {
|
||||
'path': "/admin/users/%s/log_out",
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
'external': {
|
||||
'path': "/users/by-external/%s.json",
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class DiscourseManager:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -148,56 +28,14 @@ class DiscourseManager:
|
||||
SUSPEND_DAYS = 99999
|
||||
SUSPEND_REASON = "Disabled by auth."
|
||||
|
||||
@staticmethod
|
||||
def __exc(endpoint, *args, **kwargs):
|
||||
params = {
|
||||
'api_key': settings.DISCOURSE_API_KEY,
|
||||
'api_username': settings.DISCOURSE_API_USERNAME,
|
||||
}
|
||||
silent = kwargs.pop('silent', False)
|
||||
if args:
|
||||
endpoint['parsed_url'] = endpoint['path'] % args
|
||||
else:
|
||||
endpoint['parsed_url'] = endpoint['path']
|
||||
data = {}
|
||||
for arg in endpoint['args']['required']:
|
||||
data[arg] = kwargs[arg]
|
||||
for arg in endpoint['args']['optional']:
|
||||
if arg in kwargs:
|
||||
data[arg] = kwargs[arg]
|
||||
for arg in kwargs:
|
||||
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
|
||||
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
||||
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], headers=params,
|
||||
json=data)
|
||||
try:
|
||||
if 'errors' in r.json() and not silent:
|
||||
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
||||
raise DiscourseError(endpoint, r.json()['errors'])
|
||||
if 'success' in r.json():
|
||||
if not r.json()['success'] and not silent:
|
||||
raise DiscourseError(endpoint, None)
|
||||
out = r.json()
|
||||
except ValueError:
|
||||
out = r.text
|
||||
finally:
|
||||
try:
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise DiscourseError(endpoint, e.response.status_code)
|
||||
logger.debug("Discourse API output:\n{}".format(out)) # this is spamy as hell remove before release
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _get_groups():
|
||||
endpoint = ENDPOINTS['groups']['list']
|
||||
data = DiscourseManager.__exc(endpoint)
|
||||
data = providers.discourse.client.groups()
|
||||
return [g for g in data if not g['automatic']]
|
||||
|
||||
@staticmethod
|
||||
def _create_group(name):
|
||||
endpoint = ENDPOINTS['groups']['create']
|
||||
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
|
||||
return providers.discourse.client.create_group(name=name[:20], visible=True)['basic_group']
|
||||
|
||||
@staticmethod
|
||||
def _generate_cache_group_name_key(name):
|
||||
@@ -235,13 +73,11 @@ class DiscourseManager:
|
||||
|
||||
@staticmethod
|
||||
def __add_user_to_group(g_id, username):
|
||||
endpoint = ENDPOINTS['groups']['add_user']
|
||||
DiscourseManager.__exc(endpoint, g_id, usernames=username)
|
||||
providers.discourse.client.add_group_member(g_id, username)
|
||||
|
||||
@staticmethod
|
||||
def __remove_user_from_group(g_id, username):
|
||||
endpoint = ENDPOINTS['groups']['remove_user']
|
||||
DiscourseManager.__exc(endpoint, g_id, username=username)
|
||||
def __remove_user_from_group(g_id, uid):
|
||||
providers.discourse.client.delete_group_member(g_id, uid)
|
||||
|
||||
@staticmethod
|
||||
def __generate_group_dict(names):
|
||||
@@ -253,39 +89,35 @@ class DiscourseManager:
|
||||
@staticmethod
|
||||
def __get_user_groups(username):
|
||||
data = DiscourseManager.__get_user(username)
|
||||
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
|
||||
return [g['id'] for g in data['groups'] if not g['automatic']]
|
||||
|
||||
@staticmethod
|
||||
def __user_name_to_id(name, silent=False):
|
||||
data = DiscourseManager.__get_user(name, silent=silent)
|
||||
data = DiscourseManager.__get_user(name)
|
||||
return data['user']['id']
|
||||
|
||||
@staticmethod
|
||||
def __get_user(username, silent=False):
|
||||
endpoint = ENDPOINTS['users']['get']
|
||||
return DiscourseManager.__exc(endpoint, username, silent=silent)
|
||||
return providers.discourse.client.user(username)
|
||||
|
||||
@staticmethod
|
||||
def __activate_user(username):
|
||||
endpoint = ENDPOINTS['users']['activate']
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, u_id)
|
||||
providers.discourse.client.activate(u_id)
|
||||
|
||||
@staticmethod
|
||||
def __update_user(username, **kwargs):
|
||||
endpoint = ENDPOINTS['users']['update']
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
|
||||
providers.discourse.client.update_user(endpoint, u_id, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def __create_user(username, email, password):
|
||||
endpoint = ENDPOINTS['users']['create']
|
||||
DiscourseManager.__exc(endpoint, name=username, username=username, email=email, password=password, active=True)
|
||||
providers.discourse.client.create_user(username, username, email, password)
|
||||
|
||||
@staticmethod
|
||||
def __check_if_user_exists(username):
|
||||
try:
|
||||
DiscourseManager.__user_name_to_id(username, silent=True)
|
||||
DiscourseManager.__user_name_to_id(username)
|
||||
return True
|
||||
except DiscourseError:
|
||||
return False
|
||||
@@ -293,30 +125,26 @@ class DiscourseManager:
|
||||
@staticmethod
|
||||
def __suspend_user(username):
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
endpoint = ENDPOINTS['users']['suspend']
|
||||
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
|
||||
reason=DiscourseManager.SUSPEND_REASON)
|
||||
return providers.discourse.client.suspend(u_id, DiscourseManager.SUSPEND_DAYS,
|
||||
DiscourseManager.SUSPEND_REASON)
|
||||
|
||||
@staticmethod
|
||||
def __unsuspend(username):
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
endpoint = ENDPOINTS['users']['unsuspend']
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
return providers.discourse.client.unsuspend(u_id)
|
||||
|
||||
@staticmethod
|
||||
def __set_email(username, email):
|
||||
endpoint = ENDPOINTS['users']['set_email']
|
||||
return DiscourseManager.__exc(endpoint, username, email=email)
|
||||
return providers.discourse.client.update_email(username, email)
|
||||
|
||||
@staticmethod
|
||||
def __logout(u_id):
|
||||
endpoint = ENDPOINTS['users']['logout']
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
return providers.discourse.client.log_out(u_id)
|
||||
|
||||
@staticmethod
|
||||
def __get_user_by_external(u_id):
|
||||
endpoint = ENDPOINTS['users']['external']
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
data = providers.discourse.client.user_by_external_id(u_id)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def __user_id_by_external_id(u_id):
|
||||
@@ -352,7 +180,9 @@ class DiscourseManager:
|
||||
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
||||
group_dict = DiscourseManager.__generate_group_dict(groups)
|
||||
inv_group_dict = {v: k for k, v in group_dict.items()}
|
||||
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
|
||||
discord_user = DiscourseManager.__get_user_by_external(user.pk)
|
||||
username = discord_user['username']
|
||||
uid = discord_user['id']
|
||||
user_groups = DiscourseManager.__get_user_groups(username)
|
||||
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
||||
rem_groups = [x for x in user_groups if x not in inv_group_dict]
|
||||
@@ -365,7 +195,7 @@ class DiscourseManager:
|
||||
logger.info(
|
||||
"Updating discourse user %s groups: removing %s" % (username, rem_groups))
|
||||
for g in rem_groups:
|
||||
DiscourseManager.__remove_user_from_group(g, username)
|
||||
DiscourseManager.__remove_user_from_group(g, uid)
|
||||
|
||||
@staticmethod
|
||||
def disable_user(user):
|
||||
|
||||
@@ -16,3 +16,4 @@ class DiscourseUser(models.Model):
|
||||
permissions = (
|
||||
("access_discourse", u"Can access the Discourse service"),
|
||||
)
|
||||
|
||||
|
||||
19
allianceauth/services/modules/discourse/providers.py
Normal file
19
allianceauth/services/modules/discourse/providers.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pydiscourse import DiscourseClient
|
||||
from django.conf import settings
|
||||
|
||||
class DiscourseAPIClient():
|
||||
_client = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self._client:
|
||||
self._client = DiscourseClient(
|
||||
settings.DISCOURSE_URL,
|
||||
api_username=settings.DISCOURSE_API_USERNAME,
|
||||
api_key=settings.DISCOURSE_API_KEY)
|
||||
return self._client
|
||||
|
||||
discourse = DiscourseAPIClient()
|
||||
@@ -47,7 +47,8 @@ class DiscourseTasks:
|
||||
logger.debug("Updating discourse groups for user %s" % user)
|
||||
try:
|
||||
DiscourseManager.update_groups(user)
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||
raise self.retry(countdown=60 * 10)
|
||||
logger.debug("Updated user %s discourse groups." % user)
|
||||
@@ -63,3 +64,4 @@ class DiscourseTasks:
|
||||
def get_username(user):
|
||||
from .auth_hooks import DiscourseService
|
||||
return NameFormatter(DiscourseService(), user).format_name()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.11 on 2020-05-22 13:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mumble', '0009_set_mumble_dissplay_names'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='mumbleuser',
|
||||
name='certhash',
|
||||
field=models.CharField(blank=True, editable=False, help_text='Hash of Mumble client certificate as presented when user authenticates', max_length=254, null=True, verbose_name='Certificate Hash'),
|
||||
),
|
||||
]
|
||||
@@ -66,11 +66,18 @@ class MumbleUser(AbstractServiceModel):
|
||||
pwhash = models.CharField(max_length=80)
|
||||
hashfn = models.CharField(max_length=20, default='sha1')
|
||||
groups = models.TextField(blank=True, null=True)
|
||||
certhash = models.CharField(
|
||||
verbose_name="Certificate Hash",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Hash of Mumble client certificate as presented when user authenticates"
|
||||
)
|
||||
display_name = models.CharField(max_length=254, unique=True)
|
||||
|
||||
objects = MumbleManager()
|
||||
|
||||
display_name = models.CharField(max_length=254, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class JabberBroadcast(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
_('Jabber Broadcast'),
|
||||
'fa fa-lock fa-fw fa-bullhorn',
|
||||
'fas fa-bullhorn fa-fw',
|
||||
'openfire:broadcast')
|
||||
|
||||
def render(self, request):
|
||||
@@ -89,7 +89,7 @@ class FleetBroadcastFormatter(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
_('Fleet Broadcast Formatter'),
|
||||
'fa fa-lock fa-fw fa-space-shuttle',
|
||||
'fas fa-space-shuttle fa-fw',
|
||||
'services:fleet_format_tool')
|
||||
|
||||
def render(self, request):
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.contrib.auth.models import User
|
||||
from .hooks import ServicesHook
|
||||
from celery_once import QueueOnce as BaseTask, AlreadyQueued
|
||||
from django.core.cache import cache
|
||||
from time import time
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -22,14 +21,9 @@ class DjangoBackend:
|
||||
|
||||
@staticmethod
|
||||
def raise_or_lock(key, timeout):
|
||||
now = int(time())
|
||||
result = cache.get(key)
|
||||
if result:
|
||||
remaining = int(result) - now
|
||||
if remaining > 0:
|
||||
raise AlreadyQueued(remaining)
|
||||
else:
|
||||
cache.set(key, now + timeout, timeout)
|
||||
acquired = cache.add(key=key, value="lock", timeout=timeout)
|
||||
if not acquired:
|
||||
raise AlreadyQueued(int(cache.ttl(key)))
|
||||
|
||||
@staticmethod
|
||||
def clear_lock(key):
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
{% load navactive %}
|
||||
|
||||
<li>
|
||||
<a class="{% navactive request item.navactive|join:" " %}" href="{% url item.url_name %}">
|
||||
<i class="{{ item.classes }}"></i> {% trans item.text %}
|
||||
<a class="{% navactive request item.navactive|join:' ' %}" href="{% url item.url_name %}">
|
||||
<i class="{{ item.classes }}"></i> {% trans item.text %}
|
||||
{% if item.count != None %}
|
||||
<span class="badge">{{ item.count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -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)
|
||||
@@ -1,11 +1,15 @@
|
||||
from unittest import mock
|
||||
|
||||
from celery_once import AlreadyQueued
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from allianceauth.services.tasks import validate_services
|
||||
|
||||
from ..tasks import DjangoBackend
|
||||
|
||||
|
||||
class ServicesTasksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
@@ -24,3 +28,46 @@ class ServicesTasksTestCase(TestCase):
|
||||
self.assertTrue(svc.validate_user.called)
|
||||
args, kwargs = svc.validate_user.call_args
|
||||
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
|
||||
|
||||
|
||||
class TestDjangoBackend(TestCase):
|
||||
|
||||
TEST_KEY = "my-django-backend-test-key"
|
||||
TIMEOUT = 1800
|
||||
|
||||
def setUp(self) -> None:
|
||||
cache.delete(self.TEST_KEY)
|
||||
self.backend = DjangoBackend(dict())
|
||||
|
||||
def test_can_get_lock(self):
|
||||
"""
|
||||
when lock can be acquired
|
||||
then set it with timetout
|
||||
"""
|
||||
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||
self.assertIsNotNone(cache.get(self.TEST_KEY))
|
||||
self.assertAlmostEqual(cache.ttl(self.TEST_KEY), self.TIMEOUT, delta=2)
|
||||
|
||||
def test_when_cant_get_lock_raise_exception(self):
|
||||
"""
|
||||
when lock can bot be acquired
|
||||
then raise AlreadyQueued exception with countdown
|
||||
"""
|
||||
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||
|
||||
try:
|
||||
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||
except Exception as ex:
|
||||
self.assertIsInstance(ex, AlreadyQueued)
|
||||
self.assertAlmostEqual(ex.countdown, self.TIMEOUT, delta=2)
|
||||
|
||||
def test_can_clear_lock(self):
|
||||
"""
|
||||
when a lock exists
|
||||
then can get a new lock after clearing it
|
||||
"""
|
||||
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||
|
||||
self.backend.clear_lock(self.TEST_KEY)
|
||||
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||
self.assertIsNotNone(cache.get(self.TEST_KEY))
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from allianceauth import hooks
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
|
||||
from . import urls
|
||||
from .managers import SRPManager
|
||||
|
||||
|
||||
class SrpMenu(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self, _('Ship Replacement'),
|
||||
'fa fa-money fa-fw',
|
||||
'far fa-money-bill-alt fa-fw',
|
||||
'srp:management',
|
||||
navactive=['srp:'])
|
||||
|
||||
def render(self, request):
|
||||
if request.user.has_perm('srp.access_srp'):
|
||||
app_count = SRPManager.pending_requests_count_for_user(request.user)
|
||||
self.count = app_count if app_count and app_count > 0 else None
|
||||
return MenuItemHook.render(self, request)
|
||||
return ''
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth import NAME
|
||||
from allianceauth.eveonline.providers import provider
|
||||
|
||||
from .models import SrpUserRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,3 +52,12 @@ class SRPManager:
|
||||
return ship_type, ship_value, victim_id
|
||||
else:
|
||||
raise ValueError("Invalid Kill ID or Hash.")
|
||||
|
||||
@staticmethod
|
||||
def pending_requests_count_for_user(user: User):
|
||||
"""returns the number of open SRP requests for given user
|
||||
or None if user has no permission"""
|
||||
if user.has_perm("auth.srp_management"):
|
||||
return SrpUserRequest.objects.filter(srp_status="pending").count()
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">{% blocktrans %}Give this link to the line members{% endblocktrans %}.</div>
|
||||
<div class="alert alert-info" role="alert">
|
||||
http://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
|
||||
{{ request.scheme }}://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
|
||||
<div class="text-center">
|
||||
<a href="{% url 'srp:management' %}" class="btn btn-primary btn-lg">{% trans "Continue" %}</a>
|
||||
</div>
|
||||
@@ -34,7 +34,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -46,8 +45,15 @@
|
||||
{% block extra_script %}
|
||||
|
||||
$('#id_fleet_time').datetimepicker({
|
||||
maskInput: true,
|
||||
format: 'Y-m-d H:i'
|
||||
setlocale: '{{ LANGUAGE_CODE }}',
|
||||
{% if NIGHT_MODE %}
|
||||
theme: 'dark',
|
||||
{% else %}
|
||||
theme: 'default',
|
||||
{% endif %}
|
||||
mask: true,
|
||||
format: 'Y-m-d H:i',
|
||||
minDate: 0
|
||||
});
|
||||
|
||||
{% endblock extra_script %}
|
||||
|
||||
@@ -131,7 +131,7 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
|
||||
<div class="checkbox">
|
||||
<label style="font-size: 1.5em">
|
||||
<input type="checkbox" name="{{srpfleetrequest.id}}">
|
||||
<span class="cr"><i class="cr-icon fa fa-check"></i></span>
|
||||
<span class="cr"><i class="cr-icon fas fa-check"></i></span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch, Mock
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..managers import SRPManager
|
||||
from ..models import SrpUserRequest, SrpFleetMain
|
||||
|
||||
MODULE_PATH = 'allianceauth.srp.managers'
|
||||
|
||||
@@ -13,6 +18,7 @@ currentdir = os.path.dirname(os.path.abspath(inspect.getfile(
|
||||
inspect.currentframe()
|
||||
)))
|
||||
|
||||
|
||||
def load_data(filename):
|
||||
"""loads given JSON file from `testdata` sub folder and returns content"""
|
||||
with open(
|
||||
@@ -52,7 +58,7 @@ class TestSrpManager(TestCase):
|
||||
mock_get.return_value.json.return_value = ['']
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
|
||||
SRPManager.get_kill_data(81973979)
|
||||
|
||||
@patch(MODULE_PATH + '.provider')
|
||||
@patch(MODULE_PATH + '.requests.get')
|
||||
@@ -67,6 +73,34 @@ class TestSrpManager(TestCase):
|
||||
result.return_value = None
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
|
||||
SRPManager.get_kill_data(81973979)
|
||||
|
||||
|
||||
def test_pending_requests_count_for_user(self):
|
||||
user = AuthUtils.create_member("Bruce Wayne")
|
||||
|
||||
# when no permission to approve SRP requests
|
||||
# then return None
|
||||
self.assertIsNone(SRPManager.pending_requests_count_for_user(user))
|
||||
|
||||
# given permission to approve SRP requests
|
||||
# when no open requests
|
||||
# then return 0
|
||||
AuthUtils.add_permission_to_user_by_name("auth.srp_management", user)
|
||||
user = User.objects.get(pk=user.pk)
|
||||
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 0)
|
||||
|
||||
# given permission to approve SRP requests
|
||||
# when 1 pending request
|
||||
# then return 1
|
||||
fleet = SrpFleetMain.objects.create(fleet_time=now())
|
||||
SrpUserRequest.objects.create(
|
||||
killboard_link="https://zkillboard.com/kill/79111612/",
|
||||
srp_status="Pending",
|
||||
srp_fleet_main=fleet,
|
||||
)
|
||||
SrpUserRequest.objects.create(
|
||||
killboard_link="https://zkillboard.com/kill/79111612/",
|
||||
srp_status="Approved",
|
||||
srp_fleet_main=fleet,
|
||||
)
|
||||
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 1)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Import the fonts from CDN
|
||||
@font-face {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot');
|
||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
|
||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot');
|
||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff') format('woff'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user