Compare commits

..

77 Commits

Author SHA1 Message Date
col_crunch
39f7f32b7d Version bump. 2020-07-13 15:32:54 -04:00
colcrunch
b4522a1277 Merge branch 'enable_django_esi_20' into 'master'
Enable django-esi 2.0 dependency

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

Closes #1250

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

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

See merge request allianceauth/allianceauth!1228
2020-06-22 03:19:27 +00:00
Carter Foulger
c970cbbd2d Updated discord.md with additional troubleshooting
steps.
2020-06-19 19:45:22 +00:00
Ariel Rin
9ea55fa51f Version Bump 2.7.2 2020-06-11 03:49:06 +00:00
Ariel Rin
5775a11b4e Merge branch 'replace_context_manager_groups' into 'master'
Improve page load performance by replacing groups context manager

See merge request allianceauth/allianceauth!1219
2020-06-11 03:41:31 +00:00
Ariel Rin
1a666b6584 Merge branch 'fontawesomev5' into 'master'
Font Awesome V5 Update

Closes #1207

See merge request allianceauth/allianceauth!1224
2020-06-11 03:33:37 +00:00
Ariel Rin
35407a2108 Font Awesome V5 Update 2020-06-11 03:33:37 +00:00
Ariel Rin
71fb19aa22 Merge branch 'version_battle' into 'master'
Make version relevant to an admin

See merge request allianceauth/allianceauth!1220
2020-06-11 03:13:13 +00:00
AaronKable
b7d7f7b8ce latest stable 2020-06-11 10:47:05 +08:00
Ariel Rin
59b983edcc Merge branch 'future' into 'master'
Remove Future dependency

Closes #1242

See merge request allianceauth/allianceauth!1223
2020-06-11 01:01:23 +00:00
Ariel Rin
1734d034e1 Merge branch 'evemodel_integers' into 'master'
Change EveModels to Integer ID fields

See merge request allianceauth/allianceauth!1211
2020-06-09 13:10:10 +00:00
Aaron Kable
7f7500ff0c Change EveModels to Integer ID fields 2020-06-09 13:10:10 +00:00
Ariel Rin
ce77c24e5c Exclude Celery 4.4.4 2020-06-09 11:30:13 +10:00
Ariel Rin
5469a591c0 Remove Future dependency 2020-06-09 11:10:32 +10:00
Ariel Rin
a4befc5e59 Version Bump 2.7.1 2020-06-09 00:25:30 +00:00
Ariel Rin
1ee8065592 Merge branch 'issue_1244' into 'master'
Fix sleep length must be non-negative

Closes #1244

See merge request allianceauth/allianceauth!1222
2020-06-09 00:23:36 +00:00
ErikKalkoken
e4e3bd44fc Fix sleep length must be non-negative 2020-06-08 14:59:22 +02:00
AaronKable
c75de07c2e Only show Pre-Release when available 2020-06-08 20:47:05 +08:00
AaronKable
e928131809 make version relevant to an admin 2020-06-08 19:18:11 +08:00
Ariel Rin
4f802e82a9 Version Bump to 2.7.0 2020-06-07 06:53:52 +00:00
Ariel Rin
0c90bd462e Merge branch 'remove_test_logging' into 'master'
Remove Test Logging from discourse

See merge request allianceauth/allianceauth!1207
2020-06-07 06:52:08 +00:00
ErikKalkoken
bbb70c93d9 Initial 2020-06-06 17:59:23 +02:00
Ariel Rin
f6e6ba775c Merge branch 'revert-3a984e8a' into 'master'
Revert "Merge branch 'notifications_refresh' into 'master'"

See merge request allianceauth/allianceauth!1216
2020-06-04 14:22:11 +00:00
Ariel Rin
06646be907 Merge branch 'fix_celery_4.4.4_issue' into 'master'
Add future to dependencies to fix celery 4.4.4 issue

See merge request allianceauth/allianceauth!1217
2020-06-04 11:58:28 +00:00
ErikKalkoken
1b4c1a4b9e Add future to dependencies 2020-06-04 13:52:34 +02:00
Ariel Rin
ae3f5a0f62 Revert "Merge branch 'notifications_refresh' into 'master'"
This reverts merge request !1215
2020-06-04 11:21:50 +00:00
Ariel Rin
3a984e8a4d Merge branch 'notifications_refresh' into 'master'
Add notifications auto refresh

See merge request allianceauth/allianceauth!1215
2020-06-04 08:25:01 +00:00
Erik Kalkoken
7d711a54bc Add notifications auto refresh 2020-06-04 08:25:01 +00:00
Ariel Rin
d92d629c25 Merge branch 'orm_fix' into 'master'
Fix Group Managment ORM Queries

See merge request allianceauth/allianceauth!1214
2020-05-27 02:23:37 +00:00
Ariel Rin
21e630209a Merge branch 'issue_1238' into 'master'
PEP440 versioning for admin dashboard

See merge request allianceauth/allianceauth!1213
2020-05-27 02:21:01 +00:00
Erik Kalkoken
e3933998ef PEP440 versioning for admin dashboard 2020-05-27 02:21:00 +00:00
Ariel Rin
667afe9051 Merge branch 'master' into 'master'
Update Extension Logger

Closes #1230

See merge request allianceauth/allianceauth!1206
2020-05-27 02:17:43 +00:00
AaronKable
26dc2881eb fix group managment ORM queries 2020-05-27 08:15:22 +08:00
Col Crunch
250cb33285 Raise an error if get_extension_logger recieves a non-string argument. 2020-05-26 13:49:06 -04:00
Col Crunch
db51abec1f Change default logging level for extension logger. 2020-05-26 13:26:19 -04:00
Col Crunch
530716d458 Fix docstring n get_extension_logger 2020-05-26 13:25:48 -04:00
Ariel Rin
f3065d79b3 Merge branch 'improve_evelinks' into 'master'
Add support for type icons to evelinks

See merge request allianceauth/allianceauth!1210
2020-05-25 14:51:42 +00:00
Erik Kalkoken
bca5f0472e Add support for type icons to evelinks 2020-05-25 14:51:41 +00:00
Ariel Rin
8e54c43917 Merge branch 'mumble-certhash' into 'master'
Add certhash field to Mumble user

See merge request allianceauth/allianceauth!1204
2020-05-25 14:46:03 +00:00
Ariel Rin
946df1d7a0 Merge branch 'issue_1234' into 'master'
Fix issue #1234 and add badges to group requests

Closes #1234

See merge request allianceauth/allianceauth!1212
2020-05-25 13:36:29 +00:00
ErikKalkoken
55f00f742c Fix issue #1234 and add badges to group requests 2020-05-25 15:22:08 +02:00
Ariel Rin
97b2cb71b7 Version Bump to 2.6.6a10 2020-05-25 12:55:05 +00:00
Ariel Rin
ba3a5ba53c Merge branch 'discord_delete_user_bug' into 'master'
Fix API exception handling for delete_user

See merge request allianceauth/allianceauth!1209
2020-05-25 01:28:25 +00:00
ErikKalkoken
953c09c999 Fix backoff exception handling for delete_user 2020-05-24 19:13:31 +02:00
Ariel Rin
b4cc325b07 Merge branch 'fix_test_order_issues' into 'master'
Make notification tests less order dependent

See merge request allianceauth/allianceauth!1208
2020-05-23 04:58:07 +00:00
AaronKable
28c1343f3e make tests less order dependant 2020-05-23 12:54:09 +08:00
Col Crunch
c16fd94c4a Update CI config for new version of gitlab. 2020-05-23 00:31:27 -04:00
Ariel Rin
bb2cc20838 Merge branch 'discord_ignore_managed_roles' into 'master'
Enable Discord service to deal with managed roles

See merge request allianceauth/allianceauth!1205
2020-05-23 04:28:01 +00:00
Erik Kalkoken
7b815fd010 Enable Discord service to deal with managed roles 2020-05-23 04:28:01 +00:00
AaronKable
18584974df remove test logging 2020-05-23 12:18:05 +08:00
Ariel Rin
15823b7785 Merge branch 'stop_context_spam' into 'master'
Stop the notification context provider from hitting the database for every menu hook

See merge request allianceauth/allianceauth!1201
2020-05-23 04:14:10 +00:00
Col Crunch
6c275d4cd2 Add comment to local describing how to change logger level. Also update docs to be consistent with this change. 2020-05-23 00:08:37 -04:00
Col Crunch
2d64ee5e2a Base extension logger level on the level of its parent. 2020-05-23 00:08:00 -04:00
AaronKable
3ae5ffa3f6 tests 2020-05-23 12:04:06 +08:00
Ben Cole
57d9ddc2c6 Add certhash field to Mumble user 2020-05-22 14:04:35 +01:00
Ariel Rin
8b84def494 Merge branch 'discord_update_usernames' into 'master'
Add update username feature

See merge request allianceauth/allianceauth!1203
2020-05-19 03:18:01 +00:00
ErikKalkoken
546f01ceb2 Add update username feature 2020-05-19 00:13:19 +02:00
Aaron Kable
1ce0dbde0e stop the notification context profider from hitting the database for each menu hook 2020-03-16 02:09:24 +00:00
104 changed files with 4148 additions and 1454 deletions

2
.gitignore vendored
View File

@@ -75,3 +75,5 @@ celerybeat-schedule
#other
.flake8
.pylintrc
Makefile

View File

@@ -1,8 +1,11 @@
stages:
- "test"
- test
- deploy
before_script:
- apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes
- redis-cli ping
- python -V
- pip install wheel tox
@@ -47,5 +50,5 @@ deploy_production:
- python setup.py sdist
- twine upload dist/*
only:
- tags
rules:
- if: $CI_COMMIT_TAG

View File

@@ -1,14 +0,0 @@
[MASTER]
ignore-patterns=test_.*.py,__init__.py,generate_.*.py
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,x,f,ex
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
[MESSAGES CONTROL]
disable=R,C

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,275 @@
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
context = {}
result = status_overview(context)
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())
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)
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)

View File

@@ -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:'])

View File

@@ -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()

View File

@@ -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"

View File

@@ -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)

View 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'
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View 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),
),
]

View 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'),
),
]

View File

@@ -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)

View File

@@ -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 ''

View File

@@ -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

View File

@@ -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'
)

View File

@@ -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')

View File

@@ -1,10 +1,14 @@
from unittest.mock import patch, Mock
from unittest.mock import patch
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,42 +17,33 @@ 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')
@@ -63,25 +58,25 @@ class TestTasks(TestCase):
EveCharacter.objects.all().delete()
EveCorporationInfo.objects.create(
corporation_id='2345',
corporation_id=2345,
corporation_name='corp.name',
corporation_ticker='corp.ticker',
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',
executor_corp_id='78910',
)
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='c.c.t', # max 5 chars
alliance_id='character.alliance.id',
alliance_id=3456,
alliance_name='character.alliance.name',
)
@@ -89,22 +84,15 @@ class TestTasks(TestCase):
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
)
self.assertEqual(mock_update_character.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_character.apply_async.call_args[1]['args'][0]),
1234
int(mock_update_character.apply_async.call_args[1]['args'][0]), 1234
)

View File

@@ -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:'])

View File

@@ -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)}

View File

@@ -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>

View File

@@ -36,7 +36,7 @@
<tr>
<td class="text-right">
{% if member.is_leader %}
<i class="fa fa-star"></i>&nbsp;
<i class="fas fa-star"></i>&nbsp;
{% 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">

View File

@@ -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">

View File

@@ -0,0 +1,15 @@
from django import template
from django.contrib.auth.models import User
from allianceauth.groupmanagement.managers import GroupManager
register = template.Library()
@register.filter
def can_manage_groups(user: User) -> bool:
"""returns True if the given user can manage groups. Returns False otherwise."""
if not isinstance(user, User):
return False
return GroupManager.can_manage_groups(user)

View File

@@ -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)

View File

@@ -0,0 +1,27 @@
from unittest.mock import patch
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..templatetags.groupmanagement import can_manage_groups
MODULE_PATH = 'allianceauth.groupmanagement.templatetags.groupmanagement'
@patch(MODULE_PATH + '.GroupManager.can_manage_groups')
class TestCanManageGroups(TestCase):
def setUp(self):
self.user = AuthUtils.create_user('Bruce Wayne')
def test_return_normal_result(self, mock_can_manage_groups):
mock_can_manage_groups.return_value = True
self.assertTrue(can_manage_groups(self.user))
self.assertTrue(mock_can_manage_groups.called)
def test_return_false_if_not_user(self, mock_can_manage_groups):
mock_can_manage_groups.return_value = True
self.assertFalse(can_manage_groups('invalid'))
self.assertFalse(mock_can_manage_groups.called)

View File

@@ -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({

View File

@@ -8,7 +8,7 @@ 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:'])

View File

@@ -1,5 +1,11 @@
from .models import Notification
from django.core.cache import cache
def user_notification_count(request):
return {'notifications': len(Notification.objects.filter(user__id=request.user.id).filter(viewed=False))}
user_id = request.user.id
notification_count = cache.get("u-note:{}".format(user_id), -1)
if notification_count<0:
notification_count = Notification.objects.filter(user__id=user_id).filter(viewed=False).count()
cache.set("u-note:{}".format(user_id),notification_count,5)
return {'notifications': notification_count}

View File

@@ -1 +0,0 @@
# Create your tests here.

View File

@@ -0,0 +1,76 @@
from unittest import mock
from django.test import TestCase
from allianceauth.notifications.context_processors import user_notification_count
from allianceauth.tests.auth_utils import AuthUtils
from django.core.cache import cache
from allianceauth.notifications.models import Notification
class TestNotificationCount(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('magic_mike')
AuthUtils.add_main_character(cls.user, 'Magic Mike', '1', corp_id='2', corp_name='Pole Riders', corp_ticker='PRIDE', alliance_id='3', alliance_name='RIDERS')
cls.user.profile.refresh_from_db()
### test notifications for mike
Notification.objects.all().delete()
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 1 Failed",
message="Because it was broken",
viewed=True)
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 2 Failed",
message="Because it was broken")
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 3 Failed",
message="Because it was broken")
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 4 Failed",
message="Because it was broken")
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 5 Failed",
message="Because it was broken")
Notification.objects.create(user=cls.user,
level="INFO",
title="Job 6 Failed",
message="Because it was broken")
cls.user2 = AuthUtils.create_user('teh_kid')
AuthUtils.add_main_character(cls.user, 'The Kid', '2', corp_id='2', corp_name='Pole Riders', corp_ticker='PRIDE', alliance_id='3', alliance_name='RIDERS')
cls.user2.profile.refresh_from_db()
# Noitification for kid
Notification.objects.create(user=cls.user2,
level="INFO",
title="Job 6 Failed",
message="Because it was broken")
def test_no_cache(self):
mock_req = mock.MagicMock()
mock_req.user.id = self.user.id
cache.delete("u-note:{}".format(self.user.id)) # force the db to be hit
context_dict = user_notification_count(mock_req)
self.assertIsInstance(context_dict, dict)
self.assertEqual(context_dict.get('notifications'), 5) # 5 only
@mock.patch('allianceauth.notifications.models.Notification.objects')
def test_cache(self, mock_foo):
mock_foo.filter.return_value = mock_foo
mock_foo.count.return_value = 5
mock_req = mock.MagicMock()
mock_req.user.id = self.user.id
cache.set("u-note:{}".format(self.user.id),10,5)
context_dict = user_notification_count(mock_req)
self.assertIsInstance(context_dict, dict)
self.assertEqual(context_dict.get('notifications'), 10) # cached value
self.assertEqual(mock_foo.called, 0) # ensure the DB was not hit

View File

@@ -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:'])

View File

@@ -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:'])

View File

@@ -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)

View File

@@ -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',

View File

@@ -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',

View File

@@ -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):

View File

@@ -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

View File

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

View File

@@ -1,2 +1,3 @@
from .client import DiscordClient # noqa
from .exceptions import DiscordApiBackoff # noqa
from .exceptions import DiscordApiBackoff # noqa
from .helpers import DiscordRoles # noqa

View File

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

View File

@@ -1,4 +1,5 @@
from hashlib import md5
import json
import logging
from time import sleep
from urllib.parse import urljoin
@@ -14,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,
@@ -22,6 +24,7 @@ from .app_settings import (
DISCORD_ROLES_CACHE_MAX_AGE,
)
from .exceptions import DiscordRateLimitExhausted, DiscordTooManyRequestsError
from .helpers import DiscordRoles
from ..utils import LoggerAddTag
@@ -44,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.
@@ -72,8 +78,8 @@ class DiscordClient:
_KEY_GLOBAL_BACKOFF_UNTIL = 'DISCORD_GLOBAL_BACKOFF_UNTIL'
_KEY_GLOBAL_RATE_LIMIT_REMAINING = 'DISCORD_GLOBAL_RATE_LIMIT_REMAINING'
_KEYPREFIX_GUILD_NAME = 'DISCORD_GUILD_NAME'
_KEYPREFIX_GUILD_ROLES = 'DISCORD_GUILD_ROLES'
_KEYPREFIX_ROLE_NAME = 'DISCORD_ROLE_NAME'
_ROLE_NAME_MAX_CHARS = 100
_NICK_MAX_CHARS = 32
_HTTP_STATUS_CODE_NOT_FOUND = 404
@@ -166,23 +172,7 @@ class DiscordClient:
)
return r.json()
# guild roles
def create_guild_role(self, guild_id: int, role_name: str, **kwargs) -> dict:
"""Create a new guild role with the given name.
See official documentation for additional optional parameters.
Note that Discord allows creating multiple roles with the name name,
so it's important to check existing roles before creating new one
to avoid duplicates.
return a new role object on success
"""
route = f"guilds/{guild_id}/roles"
data = {'name': self._sanitize_role_name(role_name)}
data.update(kwargs)
r = self._api_request(method='post', route=route, data=data)
return r.json()
# guild
def guild_infos(self, guild_id: int) -> dict:
"""Returns all basic infos about this guild"""
@@ -216,101 +206,134 @@ class DiscordClient:
gen_key = DiscordClient._generate_hash(f'{guild_id}')
return f'{cls._KEYPREFIX_GUILD_NAME}__{gen_key}'
def guild_roles(self, guild_id: int) -> list:
"""Returns the list of all roles for this guild"""
# guild roles
def guild_roles(self, guild_id: int, use_cache: bool = True) -> list:
"""Returns the list of all roles for this guild
If use_cache is set to False it will always hit the API to retrieve
fresh data and update the cache
"""
cache_key = self._guild_roles_cache_key(guild_id)
if use_cache:
roles_raw = self._redis.get(name=cache_key)
if roles_raw:
logger.debug('Returning roles for guild %s from cache', guild_id)
return json.loads(self._redis_decode(roles_raw))
else:
logger.debug('No roles for guild %s in cache', guild_id)
route = f"guilds/{guild_id}/roles"
r = self._api_request(method='get', route=route)
return r.json()
r = self._api_request(method='get', route=route)
roles = r.json()
if roles and isinstance(roles, list):
self._redis.set(
name=cache_key,
value=json.dumps(roles),
px=DISCORD_ROLES_CACHE_MAX_AGE
)
return roles
def create_guild_role(self, guild_id: int, role_name: str, **kwargs) -> dict:
"""Create a new guild role with the given name.
See official documentation for additional optional parameters.
Note that Discord allows the creation of multiple roles with the same name,
so to avoid duplicates it's important to check existing roles
before creating new one
returns a new role dict on success
"""
route = f"guilds/{guild_id}/roles"
data = {'name': DiscordRoles.sanitize_role_name(role_name)}
data.update(kwargs)
r = self._api_request(method='post', route=route, data=data)
role = r.json()
if role:
self._invalidate_guild_roles_cache(guild_id)
return role
def delete_guild_role(self, guild_id: int, role_id: int) -> bool:
"""Deletes a guild role"""
route = f"guilds/{guild_id}/roles/{role_id}"
r = self._api_request(method='delete', route=route)
if r.status_code == 204:
self._invalidate_guild_roles_cache(guild_id)
return True
else:
return False
def _invalidate_guild_roles_cache(self, guild_id: int) -> None:
cache_key = self._guild_roles_cache_key(guild_id)
self._redis.delete(cache_key)
logger.debug('Guild roles cache invalidated')
# guild role cache
def match_guild_roles_to_names(self, guild_id: int, role_names: list) -> list:
@classmethod
def _guild_roles_cache_key(cls, guild_id: int) -> str:
"""Returns key for accessing cached roles for a guild"""
gen_key = cls._generate_hash(f'{guild_id}')
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
def match_or_create_roles_from_names(self, guild_id: int, role_names: list) -> list:
"""returns Discord roles matching the given names
Returns as list of tuple of role and created flag
Will try to match with existing roles names
Non-existing roles will be created, then created flag will be True
Roles names are cached to improve performance
Params:
- guild_id: ID of guild
- role_names: list of name strings each defining a role
"""
roles = list()
for role_name in role_names:
role, created = self.match_guild_role_to_name(
guild_id=guild_id, role_name=self._sanitize_role_name(role_name)
guild_roles = DiscordRoles(self.guild_roles(guild_id))
role_names_cleaned = {
DiscordRoles.sanitize_role_name(name) for name in role_names
}
for role_name in role_names_cleaned:
role, created = self.match_or_create_role_from_name(
guild_id=guild_id,
role_name=DiscordRoles.sanitize_role_name(role_name),
guild_roles=guild_roles
)
if role:
roles.append((role, created))
roles.append((role, created))
if created:
guild_roles = guild_roles.union(DiscordRoles([role]))
return roles
def match_guild_role_to_name(self, guild_id: int, role_name: str) -> tuple:
def match_or_create_role_from_name(
self, guild_id: int, role_name: str, guild_roles: DiscordRoles = None
) -> tuple:
"""returns Discord role matching the given name
Returns as tuple of role and created flag
Will try to match with existing roles names
Non-existing roles will be created, then created flag will be True
Roles names are cached to improve performance
Params:
- guild_id: ID of guild
- role_name: strings defining name of a role
- guild_roles: All known guild roles as DiscordRoles object.
Helps to void redundant lookups of guild roles
when this method is used multiple times.
"""
created = False
role_name = self._sanitize_role_name(role_name)
role_id = self._redis_decode(
self._redis.get(name=self._role_cache_key(guild_id, role_name))
)
if not role_id:
role_id = None
for role in self.guild_roles(guild_id):
self._update_role_cache(guild_id, role)
if role['name'] == role_name:
role_id = role['id']
if role_id:
role = self._create_role(role_id, role_name)
if not isinstance(role_name, str):
raise TypeError('role_name must be of type string')
created = False
if guild_roles is None:
guild_roles = DiscordRoles(self.guild_roles(guild_id))
role = guild_roles.role_by_name(role_name)
if not role:
if not DISCORD_DISABLE_ROLE_CREATION:
logger.debug('Need to create missing role: %s', role_name)
role = self.create_guild_role(guild_id, role_name)
created = True
else:
if not DISCORD_DISABLE_ROLE_CREATION:
role_raw = self.create_guild_role(guild_id, role_name)
role = self._create_role(role_raw['id'], role_name)
self._update_role_cache(guild_id, role)
created = True
else:
role = None
else:
role = self._create_role(int(role_id), role_name)
return role, created
@staticmethod
def _create_role(role_id: int, role_name: str) -> dict:
return {'id': int(role_id), 'name': str(role_name)}
role = None
def _update_role_cache(self, guild_id: int, role: dict) -> bool:
"""updates role cache with given role
Returns True on success, else False or raises exception
"""
if not isinstance(role, dict):
raise TypeError('role must be a dict')
return self._redis.set(
name=self._role_cache_key(guild_id=guild_id, role_name=role['name']),
value=role['id'],
px=DISCORD_ROLES_CACHE_MAX_AGE
)
@classmethod
def _role_cache_key(cls, guild_id: int, role_name: str) -> str:
"""Returns key for accessing role given by name in the role cache"""
gen_key = DiscordClient._generate_hash(f'{guild_id}{role_name}')
return f'{cls._KEYPREFIX_ROLE_NAME}__{gen_key}'
return role, created
# guild members
@@ -518,16 +541,16 @@ 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
logger.info('%s: sending %s request to url \'%s\'', uid, method.upper(), url)
logger.debug('%s: request headers:\n%s', uid, headers)
logger.debug('%s: request headers: %s', uid, headers)
r = getattr(requests, method)(**args)
logger.debug(
'%s: returned status code %d with headers:\n%s',
'%s: returned status code %d with headers: %s',
uid,
r.status_code,
r.headers
@@ -585,18 +608,21 @@ 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 %d remaining requests until reset in %s ms',
'%s: Got one of %d remaining requests until reset in %s ms',
uid,
requests_remaining + 1,
resets_in
)
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. '
@@ -679,11 +705,6 @@ class DiscordClient:
"""make sure its a list of integers"""
return [int(role_id) for role_id in list(role_ids)]
@classmethod
def _sanitize_role_name(cls, role_name: str) -> str:
"""shortens too long strings if necessary"""
return str(role_name)[:cls._ROLE_NAME_MAX_CHARS]
@classmethod
def _sanitize_nick(cls, nick: str) -> str:
"""shortens too long strings if necessary"""

View File

@@ -0,0 +1,132 @@
from copy import copy
class DiscordRoles:
"""Container class that helps dealing with Discord roles.
Objects of this class are immutable and work in many ways like sets.
Ideally objects are initialized from raw API responses,
e.g. from DiscordClient.guild.roles()
"""
_ROLE_NAME_MAX_CHARS = 100
def __init__(self, roles_lst: list) -> None:
"""roles_lst must be a list of dict, each defining a role"""
if not isinstance(roles_lst, (list, set, tuple)):
raise TypeError('roles_lst must be of type list, set or tuple')
self._roles = dict()
self._roles_by_name = dict()
for role in list(roles_lst):
self._assert_valid_role(role)
self._roles[int(role['id'])] = role
self._roles_by_name[self.sanitize_role_name(role['name'])] = role
def __eq__(self, other):
if isinstance(other, type(self)):
return self.ids() == other.ids()
return NotImplemented
def __hash__(self):
return hash(tuple(sorted(self._roles.keys())))
def __iter__(self):
for role in self._roles.values():
yield role
def __contains__(self, item) -> bool:
return int(item) in self._roles
def __len__(self):
return len(self._roles.keys())
def has_roles(self, role_ids: set) -> bool:
"""returns true if this objects contains all roles defined by given role_ids
incl. managed roles
"""
role_ids = {int(id) for id in role_ids}
all_role_ids = self._roles.keys()
return role_ids.issubset(all_role_ids)
def ids(self) -> set:
"""return a set of all role IDs"""
return set(self._roles.keys())
def subset(self, role_ids: set = None, managed_only: bool = False) -> object:
"""returns a new object containing the subset of roles as defined
by given role IDs and/or including managed roles only
"""
if role_ids is not None:
role_ids = {int(id) for id in role_ids}
if role_ids is not None and not managed_only:
return type(self)([
role for role_id, role in self._roles.items() if role_id in role_ids
])
elif role_ids is None and managed_only:
return type(self)([
role for _, role in self._roles.items() if role['managed']
])
elif role_ids is not None and managed_only:
return type(self)([
role for role_id, role in self._roles.items()
if role_id in role_ids and role['managed']
])
else:
return copy(self)
def union(self, other: object) -> object:
"""returns a new roles object that is the union of this roles object
with other"""
return type(self)(list(self) + list(other))
def difference(self, other: object) -> object:
"""returns a new roles object that only contains the roles
that exist in the current objects, but not in other
"""
new_ids = self.ids().difference(other.ids())
return self.subset(role_ids=new_ids)
def role_by_name(self, role_name: str) -> dict:
"""returns role if one with matching name is found else an empty dict"""
role_name = self.sanitize_role_name(role_name)
if role_name in self._roles_by_name:
return self._roles_by_name[role_name]
else:
return dict()
@classmethod
def create_from_matched_roles(cls, matched_roles: list) -> None:
"""returns a new object created from the given list of matches roles
matches_roles must be a list of tuples in the form: (role, created)
"""
raw_roles = [x[0] for x in matched_roles]
return cls(raw_roles)
@staticmethod
def _assert_valid_role(role: dict):
if not isinstance(role, dict):
raise TypeError('Roles must be of type dict: %s' % role)
if 'id' not in role or 'name' not in role or 'managed' not in role:
raise ValueError('This role is not valid: %s' % role)
@classmethod
def sanitize_role_name(cls, role_name: str) -> str:
"""shortens too long strings if necessary"""
return str(role_name)[:cls._ROLE_NAME_MAX_CHARS]
def match_or_create_roles_from_names(
client: object, guild_id: int, role_names: list
) -> DiscordRoles:
"""Shortcut for getting the result of matching role names as DiscordRoles object"""
return DiscordRoles.create_from_matched_roles(
client.match_or_create_roles_from_names(
guild_id=guild_id, role_names=role_names
)
)

View File

@@ -0,0 +1,38 @@
TEST_GUILD_ID = 123456789012345678
TEST_USER_ID = 198765432012345678
TEST_USER_NAME = 'Peter Parker'
TEST_USER_DISCRIMINATOR = '1234'
TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY'
TEST_ROLE_ID = 654321012345678912
def create_role(id: int, name: str, managed=False):
return {
'id': int(id),
'name': str(name),
'managed': bool(managed)
}
def create_matched_role(role, created=False) -> tuple:
return role, created
ROLE_ALPHA = create_role(1, 'alpha')
ROLE_BRAVO = create_role(2, 'bravo')
ROLE_CHARLIE = create_role(3, 'charlie')
ROLE_MIKE = create_role(13, 'mike', True)
ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
def create_user_info(
id: int = TEST_USER_ID,
username: str = TEST_USER_NAME,
discriminator: str = TEST_USER_DISCRIMINATOR
):
return {
'id': str(id),
'username': str(username[:32]),
'discriminator': str(discriminator[:4])
}

View File

@@ -50,10 +50,10 @@ class TestDiscordApiLive(TestCase):
self.client.guild_name(DISCORD_GUILD_ID)
sleep(RATE_LIMIT_DELAY_SECS)
self.client.match_guild_role_to_name(DISCORD_GUILD_ID, 'Testrole')
self.client.match_or_create_role_from_name(DISCORD_GUILD_ID, 'Testrole')
sleep(RATE_LIMIT_DELAY_SECS)
self.client.match_guild_roles_to_names(
self.client.match_or_create_roles_from_names(
DISCORD_GUILD_ID, ['Testrole A', 'Testrole B']
)
sleep(RATE_LIMIT_DELAY_SECS)

View File

@@ -1,47 +0,0 @@
"""Load testing Discord services tasks
This script will load test the Discord service tasks.
Note that his will run against your production Auth.
To run this test start a bunch of celery workers and then run this script directly.
This script requires a user with a Discord account setup through Auth.
Please provide the respective Discord user ID by setting it as environment variable:
export DISCORD_USER_ID="123456789"
"""
import os
import sys
myauth_dir = '/home/erik997/dev/python/aa/allianceauth-dev/myauth'
sys.path.insert(0, myauth_dir)
import django # noqa: E402
# init and setup django project
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myauth.settings.local")
django.setup()
from uuid import uuid1 # noqa: E402
from django.contrib.auth.models import User # noqa: E402
# from allianceauth.services.modules.discord.tasks import update_groups # noqa: E402
if 'DISCORD_USER_ID' not in os.environ:
print('Please set DISCORD_USER_ID')
exit()
DISCORD_USER_ID = os.environ['DISCORD_USER_ID']
def run_many_updates(runs):
user = User.objects.get(discord__uid=DISCORD_USER_ID)
for _ in range(runs):
new_nick = f'Testnick {uuid1().hex}'[:32]
user.profile.main_character.character_name = new_nick
user.profile.main_character.save()
# update_groups.delay(user_pk=user.pk)
if __name__ == "__main__":
run_many_updates(20)

View File

@@ -9,7 +9,22 @@ from requests.exceptions import HTTPError
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
from ..client import DiscordClient, DURATION_CONTINGENCY, DEFAULT_BACKOFF_DELAY
from . import (
TEST_GUILD_ID,
TEST_USER_ID,
TEST_USER_NAME,
TEST_BOT_TOKEN,
TEST_ROLE_ID,
ROLE_ALPHA,
ROLE_BRAVO,
ALL_ROLES,
create_role,
create_matched_role,
create_user_info
)
from ..client import (
DiscordClient, DURATION_CONTINGENCY, DEFAULT_BACKOFF_DELAY, DiscordRoles
)
from ..exceptions import DiscordRateLimitExhausted, DiscordTooManyRequestsError
from ...utils import set_logger_to_file
@@ -19,13 +34,6 @@ logger = set_logger_to_file(
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
API_BASE_URL = 'https://discordapp.com/api/'
TEST_GUILD_ID = 123456789012345678
TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY'
TEST_USER_ID = 198765432012345678
TEST_USER_NAME = 'John Doe'
TEST_ROLE_ID = 654321012345678912
TEST_ROUTE_KEY = 'abc123'
TEST_RETRY_AFTER = 3000
@@ -42,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"""
@@ -71,13 +85,6 @@ class TestBasicsAndHelpers(TestCase):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True)
self.assertTrue(client.is_rate_limited)
def test_sanitize_role_name(self):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis)
role_name_input = 'x' * 110
role_name_expected = 'x' * 100
result = client._sanitize_role_name(role_name_input)
self.assertEqual(result, role_name_expected)
@patch(MODULE_PATH + '.caches')
def test_use_default_redis_if_none_provided(self, mock_caches):
my_redis = MagicMock(spec=Redis)
@@ -110,7 +117,7 @@ class TestOtherMethods(TestCase):
self.headers = DEFAULT_REQUEST_HEADERS
def test_user_get_current(self, requests_mocker):
expected = {'id': "123456"}
expected = create_user_info()
headers = {
'accept': 'application/json',
'authorization': 'Bearer accesstoken'
@@ -125,25 +132,6 @@ class TestOtherMethods(TestCase):
result = client.current_user()
self.assertDictEqual(result, expected)
def test_guild_create_role(self, requests_mocker):
role_name_input = 'x' * 120
role_name_used = 'x' * 100
expected = {'name': role_name_used}
def data_matcher(request):
return (json.loads(request.text) == expected)
requests_mocker.post(
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles',
request_headers=self.headers,
additional_matcher=data_matcher,
text=json.dumps(expected),
)
result = self.client.create_guild_role(
guild_id=TEST_GUILD_ID, role_name=role_name_input
)
self.assertDictEqual(result, expected)
def test_get_infos(self, requests_mocker):
expected = {
'id': TEST_GUILD_ID,
@@ -157,19 +145,93 @@ class TestOtherMethods(TestCase):
result = self.client.guild_infos(TEST_GUILD_ID)
self.assertDictEqual(result, expected)
def test_get_roles(self, requests_mocker):
expected = [
{'id': 1, 'name': 'alpha'},
{'id': 2, 'name': 'bravo'}
]
requests_mocker.get(
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles',
request_headers=self.headers,
json=expected
)
result = self.client.guild_roles(TEST_GUILD_ID)
self.assertListEqual(result, expected)
@requests_mock.Mocker()
class TestGuildRoles(TestCase):
def setUp(self):
self.url = f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles'
def test_without_cache(self, requests_mocker):
expected = [ROLE_ALPHA, ROLE_BRAVO]
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=expected
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_roles(TEST_GUILD_ID, use_cache=False)
self.assertListEqual(result, expected)
self.assertTrue(my_mock_redis.set.called)
def test_return_from_cache_if_in_cache(self, requests_mocker):
expected = [ROLE_ALPHA, ROLE_BRAVO]
my_mock_redis = MagicMock(**{
'get.return_value': json.dumps(expected).encode('utf8')
})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_roles(TEST_GUILD_ID)
self.assertEqual(result, expected)
self.assertFalse(my_mock_redis.set.called)
def test_return_from_api_and_save_to_cache_if_not_in_cache(
self, requests_mocker
):
expected = [ROLE_ALPHA, ROLE_BRAVO]
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=expected
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_roles(TEST_GUILD_ID)
self.assertEqual(result, expected)
self.assertTrue(my_mock_redis.set.called)
def test_dont_save_in_cache_if_api_returns_invalid_response_1(
self, requests_mocker
):
expected = {}
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=expected
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_roles(TEST_GUILD_ID)
self.assertEqual(result, expected)
self.assertFalse(my_mock_redis.set.called)
def test_dont_save_in_cache_if_api_returns_invalid_response_2(
self, requests_mocker
):
expected = "api returns string"
my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
requests_mocker.get(
url=self.url,
request_headers=DEFAULT_REQUEST_HEADERS,
json=expected
)
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
result = client.guild_roles(TEST_GUILD_ID)
self.assertEqual(result, expected)
self.assertFalse(my_mock_redis.set.called)
@requests_mock.Mocker()
class TestGuildMember(TestCase):
@@ -179,7 +241,7 @@ class TestGuildMember(TestCase):
self.headers = DEFAULT_REQUEST_HEADERS
def test_return_guild_member_when_ok(self, requests_mocker):
expected = {'id': TEST_USER_ID, 'name': 'John Doe'}
expected = create_user_info()
requests_mocker.get(
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/members/{TEST_USER_ID}',
request_headers=self.headers,
@@ -243,37 +305,86 @@ class TestGuildGetName(TestCase):
self.assertFalse(my_mock_redis.set.called)
@requests_mock.Mocker()
class TestCreateGuildRole(TestCase):
def setUp(self):
self.request_url = f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles'
self.my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
self.client = DiscordClient2(TEST_BOT_TOKEN, self.my_mock_redis)
def test_guild_create_role_normal(self, requests_mocker):
role_name_input = 'x' * 120
role_name_used = 'x' * 100
expected = {'name': role_name_used}
def data_matcher(request):
return (json.loads(request.text) == expected)
requests_mocker.post(
self.request_url,
request_headers=DEFAULT_REQUEST_HEADERS,
additional_matcher=data_matcher,
text=json.dumps(expected),
)
result = self.client.create_guild_role(
guild_id=TEST_GUILD_ID, role_name=role_name_input
)
self.assertDictEqual(result, expected)
self.assertTrue(self.my_mock_redis.delete.called)
def test_guild_create_role_empty_response(self, requests_mocker):
expected = {}
requests_mocker.post(
self.request_url,
request_headers=DEFAULT_REQUEST_HEADERS,
text=json.dumps(expected),
)
result = self.client.create_guild_role(
guild_id=TEST_GUILD_ID, role_name='dummy'
)
self.assertDictEqual(result, expected)
self.assertFalse(self.my_mock_redis.delete.called)
@requests_mock.Mocker()
class TestGuildDeleteRole(TestCase):
def setUp(self):
self.access_token = 'accesstoken'
self.headers = DEFAULT_REQUEST_HEADERS
def setUp(self):
self.request_url = \
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles/{TEST_ROLE_ID}'
self.client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
f'{API_BASE_URL}guilds/{TEST_GUILD_ID}/roles/{TEST_ROLE_ID}'
self.my_mock_redis = MagicMock(**{
'get.return_value': None,
'pttl.return_value': -1,
})
self.client = DiscordClient2(TEST_BOT_TOKEN, self.my_mock_redis)
def test_guild_delete_role_success(self, requests_mocker):
requests_mocker.delete(
self.request_url,
request_headers=self.headers,
request_headers=DEFAULT_REQUEST_HEADERS,
status_code=204
)
result = self.client.delete_guild_role(
guild_id=TEST_GUILD_ID, role_id=TEST_ROLE_ID
)
self.assertTrue(result)
self.assertTrue(self.my_mock_redis.delete.called)
def test_guild_delete_role_failed(self, requests_mocker):
requests_mocker.delete(
self.request_url,
request_headers=self.headers,
request_headers=DEFAULT_REQUEST_HEADERS,
status_code=200
)
result = self.client.delete_guild_role(
guild_id=TEST_GUILD_ID, role_id=TEST_ROLE_ID
)
self.assertFalse(result)
self.assertFalse(self.my_mock_redis.delete.called)
@requests_mock.Mocker()
@@ -735,141 +846,138 @@ class TestGuildMemberRemoveRole(TestCase):
@patch(MODULE_PATH + '.DiscordClient.create_guild_role')
@patch(MODULE_PATH + '.DiscordClient.guild_roles')
class TestGuildGetOrCreateRoles(TestCase):
class TestMatchGuildRolesToName(TestCase):
def test_return_id_if_role_in_cache(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_name = 'alpha'
my_mock_redis = MagicMock(**{'get.return_value': b'1'})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
mock_guild_get_roles.side_effect = RuntimeError
mock_guild_create_role.side_effect = RuntimeError
expected = ({'id': 1, 'name': 'alpha'}, False)
result = client.match_guild_role_to_name(TEST_GUILD_ID, role_name)
self.assertEqual(result, expected)
def test_return_id_for_role_known_by_api(
self, mock_guild_get_roles, mock_guild_create_role,
):
my_mock_redis = MagicMock(**{'get.return_value': None})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
mock_guild_get_roles.return_value = [
{'id': 1, 'name': 'alpha'},
{'id': 2, 'name': 'bravo'}
]
mock_guild_create_role.side_effect = RuntimeError
expected = ({'id': 1, 'name': 'alpha'}, False)
result = client.match_guild_role_to_name(TEST_GUILD_ID, 'alpha')
self.assertEqual(result, expected)
expected = ({'id': 2, 'name': 'bravo'}, False)
result = client.match_guild_role_to_name(TEST_GUILD_ID, 'bravo')
self.assertEqual(result, expected)
@patch(MODULE_PATH + '.DISCORD_DISABLE_ROLE_CREATION', False)
def test_create_role_for_role_not_known_by_api(
def test_return_role_if_known(
self, mock_guild_get_roles, mock_guild_create_role,
):
my_mock_redis = MagicMock(**{'get.return_value': None})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
mock_guild_get_roles.return_value = [
{'id': 1, 'name': 'alpha'},
{'id': 2, 'name': 'bravo'}
]
mock_guild_create_role.return_value = {'id': 3, 'name': 'charlie'}
expected = ({'id': 3, 'name': 'charlie'}, True)
result = client.match_guild_role_to_name(TEST_GUILD_ID, 'charlie')
role_name = 'alpha'
mock_guild_get_roles.return_value = ALL_ROLES
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_role_from_name(TEST_GUILD_ID, role_name)
expected = (ROLE_ALPHA, False)
self.assertEqual(result, expected)
self.assertFalse(mock_guild_create_role.called)
def test_create_role_if_not_known_and_return_it(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_name = 'echo'
new_role = create_role(5, 'echo')
mock_guild_get_roles.return_value = ALL_ROLES
mock_guild_create_role.return_value = new_role
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_role_from_name(TEST_GUILD_ID, role_name)
expected = (new_role, True)
self.assertEqual(result, expected)
self.assertTrue(mock_guild_create_role.called)
@patch(MODULE_PATH + '.DISCORD_DISABLE_ROLE_CREATION', True)
def test_return_none_if_role_creation_is_disabled(
self, mock_guild_get_roles, mock_guild_create_role,
):
my_mock_redis = MagicMock(**{'get.return_value': None})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
mock_guild_get_roles.return_value = [
{'id': 1, 'name': 'alpha'},
{'id': 2, 'name': 'bravo'}
]
mock_guild_create_role.return_value = {'id': 3, 'name': 'charlie'}
result = client.match_guild_role_to_name(TEST_GUILD_ID, 'charlie')
self.assertIsNone(result[0])
self.assertFalse(result[1])
def test_return_ids_if_role_in_cache(
self, mock_guild_get_roles, mock_guild_create_role,
):
def my_cache_get(name):
map = {
DiscordClient._role_cache_key(TEST_GUILD_ID, 'alpha'): b'1',
DiscordClient._role_cache_key(TEST_GUILD_ID, 'bravo'): b'2',
DiscordClient._role_cache_key(TEST_GUILD_ID, 'charlie'): b'3'
}
if name in map:
return map[name]
else:
return None
my_mock_redis = MagicMock(**{'get.side_effect': my_cache_get})
client = DiscordClient2(TEST_BOT_TOKEN, my_mock_redis)
mock_guild_get_roles.side_effect = RuntimeError
mock_guild_create_role.side_effect = RuntimeError
expected = [
({'id': 1, 'name': 'alpha'}, False), ({'id': 3, 'name': 'charlie'}, False)
]
result = client.match_guild_roles_to_names(TEST_GUILD_ID, ['alpha', 'charlie'])
self.assertEqual(result, expected)
@patch(MODULE_PATH + '.DiscordClient.match_guild_role_to_name')
def test_ignore_none_roles_in_guild_get_or_create_roles(
self,
mock_guild_get_or_create_role,
mock_guild_get_roles,
mock_guild_create_role,
):
def my_guild_get_or_create_role(guild_id, role_name):
if role_name == 'alpha':
return {'id': 1, 'name': 'alpha'}, False
elif role_name == 'charlie':
return None, False
else:
raise ValueError('Unknown role')
mock_guild_get_or_create_role.side_effect = my_guild_get_or_create_role
role_name = 'echo'
mock_guild_get_roles.return_value = ALL_ROLES
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_guild_roles_to_names(TEST_GUILD_ID, ['alpha', 'charlie'])
expected = [
({'id': 1, 'name': 'alpha'}, False),
]
result = client.match_or_create_role_from_name(TEST_GUILD_ID, role_name)
expected = (None, False)
self.assertEqual(result, expected)
class TestUpdateRoleCache(TestCase):
self.assertFalse(mock_guild_create_role.called)
def test_can_update_cache(self):
my_mock_redis = MagicMock()
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
role = {'id': 1, 'name': 'alpha'}
client._update_role_cache(TEST_GUILD_ID, role)
self.assertTrue(my_mock_redis.set.called)
def test_raises_exception_if_wrong_role_type(self):
my_mock_redis = MagicMock()
client = DiscordClient(TEST_BOT_TOKEN, my_mock_redis)
role = 'abc'
def test_raise_exception_if_name_has_invalid_type(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_name = ['echo']
mock_guild_get_roles.return_value = ALL_ROLES
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
with self.assertRaises(TypeError):
client._update_role_cache(TEST_GUILD_ID, role)
self.assertFalse(my_mock_redis.set.called)
client.match_or_create_role_from_name(TEST_GUILD_ID, role_name)
@patch(MODULE_PATH + '.DiscordClient.create_guild_role')
@patch(MODULE_PATH + '.DiscordClient.guild_roles')
class TestMatchGuildRolesToNames(TestCase):
def test_return_roles_if_known(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_names = ['alpha', 'bravo']
mock_guild_get_roles.return_value = ALL_ROLES
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_roles_from_names(TEST_GUILD_ID, role_names)
expected = [create_matched_role(ROLE_ALPHA), create_matched_role(ROLE_BRAVO)]
self.assertEqual(
DiscordRoles.create_from_matched_roles(result),
DiscordRoles.create_from_matched_roles(expected)
)
self.assertFalse(mock_guild_create_role.called)
def test_return_roles_if_known_and_create_if_not_known(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_names = ['alpha', 'echo']
new_role = create_role(5, 'echo')
mock_guild_get_roles.return_value = ALL_ROLES
mock_guild_create_role.return_value = new_role
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_roles_from_names(TEST_GUILD_ID, role_names)
expected = \
[create_matched_role(ROLE_ALPHA), create_matched_role(new_role, True)]
self.assertEqual(
DiscordRoles.create_from_matched_roles(result),
DiscordRoles.create_from_matched_roles(expected)
)
self.assertTrue(mock_guild_create_role.called)
@patch(MODULE_PATH + '.DISCORD_DISABLE_ROLE_CREATION', True)
def test_exclude_non_roles_from_result_list(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_names = ['alpha', 'echo']
new_role = create_role(5, 'echo')
mock_guild_get_roles.return_value = ALL_ROLES
mock_guild_create_role.return_value = new_role
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_roles_from_names(TEST_GUILD_ID, role_names)
expected = [create_matched_role(ROLE_ALPHA)]
self.assertEqual(
DiscordRoles.create_from_matched_roles(result),
DiscordRoles.create_from_matched_roles(expected)
)
self.assertFalse(mock_guild_create_role.called)
def test_consolidate_roles_of_same_name(
self, mock_guild_get_roles, mock_guild_create_role,
):
role_names = ['alpha', 'bravo', 'alpha']
mock_guild_get_roles.return_value = ALL_ROLES
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_roles_from_names(TEST_GUILD_ID, role_names)
expected = [create_matched_role(ROLE_ALPHA), create_matched_role(ROLE_BRAVO)]
self.assertEqual(
DiscordRoles.create_from_matched_roles(result),
DiscordRoles.create_from_matched_roles(expected)
)
self.assertFalse(mock_guild_create_role.called)
def test_consolidate_roles_of_same_name_after_sanitation(
self, mock_guild_get_roles, mock_guild_create_role,
):
base_role_name = 'x' * 100
new_role = create_role(77, base_role_name)
role_names = [base_role_name + '1', base_role_name + '2']
mock_guild_get_roles.return_value = ALL_ROLES + [new_role]
mock_guild_create_role.return_value = new_role
client = DiscordClient2(TEST_BOT_TOKEN, mock_redis)
result = client.match_or_create_roles_from_names(TEST_GUILD_ID, role_names)
expected = [create_matched_role(new_role)]
self.assertEqual(
DiscordRoles.create_from_matched_roles(result),
DiscordRoles.create_from_matched_roles(expected)
)
self.assertFalse(mock_guild_create_role.called)
class TestApiRequestBasics(TestCase):
def setUp(self):
@@ -885,7 +993,7 @@ class TestApiRequestBasics(TestCase):
@requests_mock.Mocker()
class TestRateLimitMechanic(TestCase):
my_role = {'id': 1, 'name': 'alpha'}
my_role = ROLE_ALPHA
@staticmethod
def my_redis_pttl(name: str):
@@ -932,6 +1040,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)
@@ -973,6 +1117,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)
@@ -1067,7 +1212,7 @@ class TestRateLimitMechanic(TestCase):
@requests_mock.Mocker()
class TestBackoffHandling(TestCase):
my_role = {'id': 1, 'name': 'alpha'}
my_role = ROLE_ALPHA
def test_dont_raise_exception_when_no_global_backoff(
self, mock_redis_decr_or_set, requests_mocker
@@ -1106,7 +1251,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)

View File

@@ -0,0 +1,238 @@
from unittest import TestCase
from . import ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ALL_ROLES, create_role
from .. import DiscordRoles
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
class TestDiscordRoles(TestCase):
def setUp(self):
self.all_roles = DiscordRoles(ALL_ROLES)
def test_can_create_simple(self):
roles_raw = [ROLE_ALPHA]
roles = DiscordRoles(roles_raw)
self.assertListEqual(list(roles), roles_raw)
def test_can_create_empty(self):
roles_raw = []
roles = DiscordRoles(roles_raw)
self.assertListEqual(list(roles), [])
def test_raises_exception_if_roles_raw_of_wrong_type(self):
with self.assertRaises(TypeError):
DiscordRoles({'id': 1})
def test_raises_exception_if_list_contains_non_dict(self):
roles_raw = [ROLE_ALPHA, 'not_valid']
with self.assertRaises(TypeError):
DiscordRoles(roles_raw)
def test_raises_exception_if_invalid_role_1(self):
roles_raw = [{'name': 'alpha', 'managed': False}]
with self.assertRaises(ValueError):
DiscordRoles(roles_raw)
def test_raises_exception_if_invalid_role_2(self):
roles_raw = [{'id': 1, 'managed': False}]
with self.assertRaises(ValueError):
DiscordRoles(roles_raw)
def test_raises_exception_if_invalid_role_3(self):
roles_raw = [{'id': 1, 'name': 'alpha'}]
with self.assertRaises(ValueError):
DiscordRoles(roles_raw)
def test_roles_are_equal(self):
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_b = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
self.assertEqual(roles_a, roles_b)
def test_roles_are_not_equal(self):
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_b = DiscordRoles([ROLE_ALPHA])
self.assertNotEqual(roles_a, roles_b)
def test_different_objects_are_not_equal(self):
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
self.assertFalse(roles_a == "invalid")
def test_len(self):
self.assertEqual(len(self.all_roles), 4)
def test_contains(self):
self.assertTrue(1 in self.all_roles)
self.assertFalse(99 in self.all_roles)
def test_sanitize_role_name(self):
role_name_input = 'x' * 110
role_name_expected = 'x' * 100
result = DiscordRoles.sanitize_role_name(role_name_input)
self.assertEqual(result, role_name_expected)
def test_objects_are_hashable(self):
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_b = DiscordRoles([ROLE_BRAVO, ROLE_ALPHA])
roles_c = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
self.assertIsNotNone(hash(roles_a))
self.assertEqual(hash(roles_a), hash(roles_b))
self.assertNotEqual(hash(roles_a), hash(roles_c))
def test_create_from_matched_roles(self):
matched_roles = [
(ROLE_ALPHA, True),
(ROLE_BRAVO, False)
]
roles = DiscordRoles.create_from_matched_roles(matched_roles)
self.assertSetEqual(roles.ids(), {1, 2})
class TestIds(TestCase):
def setUp(self):
self.all_roles = DiscordRoles(ALL_ROLES)
def test_return_role_ids_default(self):
result = self.all_roles.ids()
expected = {1, 2, 3, 13}
self.assertSetEqual(result, expected)
def test_return_role_ids_empty(self):
roles = DiscordRoles([])
self.assertSetEqual(roles.ids(), set())
class TestSubset(TestCase):
def setUp(self):
self.all_roles = DiscordRoles(ALL_ROLES)
def test_ids_only(self):
role_ids = {1, 3}
roles_subset = self.all_roles.subset(role_ids)
expected = {1, 3}
self.assertSetEqual(roles_subset.ids(), expected)
def test_ids_as_string_work_too(self):
role_ids = {'1', '3'}
roles_subset = self.all_roles.subset(role_ids)
expected = {1, 3}
self.assertSetEqual(roles_subset.ids(), expected)
def test_managed_only(self):
roles = self.all_roles.subset(managed_only=True)
expected = {13}
self.assertSetEqual(roles.ids(), expected)
def test_ids_and_managed_only(self):
role_ids = {1, 3, 13}
roles_subset = self.all_roles.subset(role_ids, managed_only=True)
expected = {13}
self.assertSetEqual(roles_subset.ids(), expected)
def test_ids_are_empty(self):
roles = self.all_roles.subset([])
expected = set()
self.assertSetEqual(roles.ids(), expected)
def test_no_parameters(self):
roles = self.all_roles.subset()
expected = {1, 2, 3, 13}
self.assertSetEqual(roles.ids(), expected)
class TestHasRoles(TestCase):
def setUp(self):
self.all_roles = DiscordRoles(ALL_ROLES)
def test_true_if_all_roles_exit(self):
self.assertTrue(self.all_roles.has_roles([1, 2]))
def test_true_if_all_roles_exit_str(self):
self.assertTrue(self.all_roles.has_roles(['1', '2']))
def test_false_if_role_does_not_exit(self):
self.assertFalse(self.all_roles.has_roles([99]))
def test_false_if_one_role_does_not_exit(self):
self.assertFalse(self.all_roles.has_roles([1, 99]))
def test_true_for_empty_roles(self):
self.assertTrue(self.all_roles.has_roles([]))
class TestGetMatchingRolesByName(TestCase):
def setUp(self):
self.all_roles = DiscordRoles(ALL_ROLES)
def test_return_role_if_matches(self):
role_name = 'alpha'
expected = ROLE_ALPHA
result = self.all_roles.role_by_name(role_name)
self.assertEqual(result, expected)
def test_return_role_if_matches_and_limit_max_length(self):
role_name = 'x' * 120
expected = create_role(77, 'x' * 100)
roles = DiscordRoles([expected])
result = roles.role_by_name(role_name)
self.assertEqual(result, expected)
def test_return_empty_if_not_matches(self):
role_name = 'lima'
expected = {}
result = self.all_roles.role_by_name(role_name)
self.assertEqual(result, expected)
class TestUnion(TestCase):
def test_distinct_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
roles_3 = roles_1.union(roles_2)
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE])
self.assertEqual(roles_3, expected)
def test_overlapping_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
roles_3 = roles_1.union(roles_2)
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
self.assertEqual(roles_3, expected)
def test_identical_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_3 = roles_1.union(roles_2)
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
self.assertEqual(roles_3, expected)
class TestDifference(TestCase):
def test_distinct_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
roles_3 = roles_1.difference(roles_2)
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
self.assertEqual(roles_3, expected)
def test_overlapping_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
roles_3 = roles_1.difference(roles_2)
expected = DiscordRoles([ROLE_ALPHA])
self.assertEqual(roles_3, expected)
def test_identical_sets(self):
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
roles_3 = roles_1.difference(roles_2)
expected = DiscordRoles([])
self.assertEqual(roles_3, expected)

View File

@@ -20,6 +20,7 @@ from .app_settings import (
DISCORD_SYNC_NAMES
)
from .discord_client import DiscordClient, DiscordApiBackoff
from .discord_client.helpers import match_or_create_roles_from_names
from .utils import LoggerAddTag
@@ -62,10 +63,12 @@ class DiscordUserManager(models.Manager):
user_id = discord_user['id']
bot_client = self._bot_client(is_rate_limited=is_rate_limited)
if group_names:
role_ids = self.model._guild_get_or_create_role_ids(
bot_client, group_names
)
if group_names:
role_ids = match_or_create_roles_from_names(
client=bot_client,
guild_id=DISCORD_GUILD_ID,
role_names=group_names
).ids()
else:
role_ids = None
@@ -124,16 +127,26 @@ 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
only checks locally, does not hit the API
"""
return True if hasattr(user, self.model.USER_RELATED_NAME) else False
if not isinstance(user, User):
return False
return self.filter(user=user).select_related('user').exists()
@classmethod
def generate_bot_add_url(cls):
@@ -166,8 +179,15 @@ class DiscordUserManager(models.Manager):
@classmethod
def server_name(cls):
"""returns the name of the Discord server"""
return cls._bot_client().guild_name(DISCORD_GUILD_ID)
"""returns the name of the current Discord server
or an empty string if the name could not be retrieved
"""
try:
server_name = cls._bot_client().guild_name(DISCORD_GUILD_ID)
except HTTPError:
server_name = ""
return server_name
@staticmethod
def _bot_client(is_rate_limited: bool = True):

View File

@@ -10,7 +10,8 @@ from allianceauth.notifications import notify
from . import __title__
from .app_settings import DISCORD_GUILD_ID
from .discord_client import DiscordClient, DiscordApiBackoff
from .discord_client import DiscordApiBackoff, DiscordRoles
from .discord_client.helpers import match_or_create_roles_from_names
from .managers import DiscordUserManager
from .utils import LoggerAddTag
@@ -66,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)
@@ -91,34 +96,107 @@ 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
- False on error or raises exception
"""
role_names = DiscordUser.objects.user_group_names(self.user)
client = DiscordUser.objects._bot_client()
requested_role_ids = self._guild_get_or_create_role_ids(client, role_names)
logger.debug(
'Requested to update groups for user %s: %s', self.user, requested_role_ids
)
success = client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=self.uid,
role_ids=requested_role_ids
)
if success:
logger.info('Groups for %s have been updated', self.user)
"""
client = DiscordUser.objects._bot_client()
member_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if member_info is None:
# User is no longer a member
return None
guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID))
logger.debug('Current guild roles: %s', guild_roles.ids())
if 'roles' in member_info:
if not guild_roles.has_roles(member_info['roles']):
guild_roles = DiscordRoles(
client.guild_roles(guild_id=DISCORD_GUILD_ID, use_cache=False)
)
if not guild_roles.has_roles(member_info['roles']):
raise RuntimeError(
'Member %s has unknown roles: %s' % (
self.user,
set(member_info['roles']).difference(guild_roles.ids())
)
)
member_roles = guild_roles.subset(member_info['roles'])
else:
logger.warning('Failed to update groups for %s', self.user)
return success
raise RuntimeError('member_info from %s is not valid' % self.user)
requested_roles = match_or_create_roles_from_names(
client=client,
guild_id=DISCORD_GUILD_ID,
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()
)
logger.debug('Current roles user %s: %s', self.user, member_roles.ids())
member_roles_managed = member_roles.subset(managed_only=True)
if requested_roles != member_roles.difference(member_roles_managed):
logger.debug('Need to update roles for user %s', self.user)
new_roles = requested_roles.union(member_roles_managed)
success = client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=self.uid,
role_ids=list(new_roles.ids())
)
if success:
logger.info('Roles for %s have been updated', self.user)
else:
logger.warning('Failed to update roles for %s', self.user)
return success
else:
logger.info('No need to update roles for user %s', self.user)
return True
def update_username(self) -> bool:
"""Updates the username incl. the discriminator
from the Discord server and saves it
Returns:
- True on success
- None if user is no longer a member of the Discord server
- False on error or raises exception
"""
client = DiscordUser.objects._bot_client()
user_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if user_info is None:
success = None
elif (
user_info
and 'user' in user_info
and 'username' in user_info['user']
and 'discriminator' in user_info['user']
):
self.username = user_info['user']['username']
self.discriminator = user_info['user']['discriminator']
self.save()
logger.info('Username for %s has been updated', self.user)
success = True
else:
logger.warning('Failed to update username for %s', self.user)
success = False
return success
def delete_user(
self, notify_user: bool = False, is_rate_limited: bool = True
self,
notify_user: bool = False,
is_rate_limited: bool = True,
handle_api_exceptions: bool = False
) -> bool:
"""Deletes the Discount user both on the server and locally
@@ -126,6 +204,8 @@ class DiscordUser(models.Model):
- notify_user: When True will sent a notification to the user
informing him about the deleting of his account
- is_rate_limited: When False will disable default rate limiting (use with care)
- handle_api_exceptions: When True method will return False
when an API exception occurs
Returns True when successful, otherwise False or raises exceptions
Return None if user does no longer exist
@@ -162,18 +242,10 @@ class DiscordUser(models.Model):
return False
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
logger.exception(
'Failed to remove user %s from Discord server: %s', self.user, ex
)
return False
@staticmethod
def _guild_get_or_create_role_ids(client: DiscordClient, role_names: list) -> list:
"""wrapper for DiscordClient.match_guild_roles_to_names()
that only returns the list of IDs
"""
return [
x[0]['id'] for x in client.match_guild_roles_to_names(
guild_id=DISCORD_GUILD_ID, role_names=role_names
)
]
if handle_api_exceptions:
logger.exception(
'Failed to remove user %s from Discord server: %s', self.user, ex
)
return False
else:
raise ex

View File

@@ -26,25 +26,39 @@ 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(
bind=True, name='discord.update_username', base=QueueOnce, max_retries=None
)
def update_username(self, user_pk: int) -> None:
"""Update locally stored Discord username from Discord server for given user
Params:
- user_pk: PK of given user
"""
_task_perform_user_action(self, user_pk, 'update_username')
@shared_task(
@@ -63,6 +77,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:
@@ -171,16 +186,45 @@ def _bulk_update_nicknames_for_users(discord_users_qs: QuerySet) -> None:
chain(update_nicknames_chain).apply_async(priority=BULK_TASK_PRIORITY)
@shared_task(name='discord.update_all_usernames')
def update_all_usernames() -> None:
"""Update all usernames for all known users with a Discord account."""
discord_users_qs = DiscordUser.objects.all()
_bulk_update_usernames_for_users(discord_users_qs)
@shared_task(name='discord.update_usernames_bulk')
def update_usernames_bulk(user_pks: list) -> None:
"""Update usernames for list of users with a Discord account in bulk."""
discord_users_qs = DiscordUser.objects\
.filter(user__pk__in=user_pks)\
.select_related()
_bulk_update_usernames_for_users(discord_users_qs)
def _bulk_update_usernames_for_users(discord_users_qs: QuerySet) -> None:
logger.info(
"Starting to bulk update discord usernames for %d users",
discord_users_qs.count()
)
update_usernames_chain = list()
for discord_user in discord_users_qs:
update_usernames_chain.append(update_username.si(discord_user.user.pk))
chain(update_usernames_chain).apply_async(priority=BULK_TASK_PRIORITY)
@shared_task(name='discord.update_all')
def update_all() -> None:
"""Updates groups and nicknames (when activated) for all users."""
discord_users_qs = DiscordUser.objects.all()
logger.info(
'Starting to bulk update all %s Discord users', discord_users_qs.count()
'Starting to bulk update all for %s Discord users', discord_users_qs.count()
)
update_all_chain = list()
for discord_user in discord_users_qs:
update_all_chain.append(update_groups.si(discord_user.user.pk))
update_all_chain.append(update_username.si(discord_user.user.pk))
if DISCORD_SYNC_NAMES:
update_all_chain.append(update_nickname.si(discord_user.user.pk))

View File

@@ -15,20 +15,20 @@
</td>
<td class="text-center">
{% if not user_has_account %}
<a href="{% url 'discord:activate' %}" title="Activate" class="btn btn-warning">
<a href="{% url 'discord:activate' %}" title="{% trans 'Join the Discord server' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-ok"></span>
</a>
{% else %}
<a href="{% url 'discord:reset' %}" title="Reset" class="btn btn-primary">
<a href="{% url 'discord:reset' %}" title="{% trans 'Leave- and rejoin the Discord Server (Reset)' %}" class="btn btn-warning">
<span class="glyphicon glyphicon-refresh"></span>
</a>
<a href="{% url 'discord:deactivate' %}" title="Deactivate" class="btn btn-danger">
<a href="{% url 'discord:deactivate' %}" title="{% trans 'Leave the Discord server' %}" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% endif %}
{% if request.user.is_superuser %}
<div class="text-center" style="padding-top:5px;">
<a type="button" class="btn btn-success" href="{% url 'discord:add_bot' %}">
<a type="button" id="btnLinkDiscordServer" class="btn btn-default" href="{% url 'discord:add_bot' %}">
{% trans "Link Discord Server" %}
</a>
</div>

View File

@@ -1,17 +1,26 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.models import Group
from allianceauth.tests.auth_utils import AuthUtils
from ..discord_client.tests import ( # noqa
TEST_GUILD_ID,
TEST_USER_ID,
TEST_USER_NAME,
TEST_USER_DISCRIMINATOR,
create_role,
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE,
ROLE_MIKE,
create_user_info
)
DEFAULT_AUTH_GROUP = 'Member'
MODULE_PATH = 'allianceauth.services.modules.discord'
TEST_GUILD_ID = 123456789012345678
TEST_USER_ID = 198765432012345678
TEST_USER_NAME = 'Peter Parker'
TEST_MAIN_NAME = 'Spiderman'
TEST_MAIN_ID = 1005
def add_permissions_to_members():
permission = Permission.objects.get(codename='access_discord')
permission = AuthUtils.get_permission_by_name('discord.access_discord')
members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
AuthUtils.add_permissions_to_groups([permission], [members])

View File

@@ -0,0 +1,83 @@
# flake8: noqa
"""Concurrency testing Discord service tasks
This script will run many Discord service tasks in parallel to test concurrency
Note that it will run against your main Auth database and not test!
Check allianceauth.log for the results.
To run this test start a bunch of celery workers and then run this script directly.
Make sure to also set the environment variable AUTH_PROJECT_PATH to your Auth path
and DJANGO_SETTINGS_MODULE to the relative location of your settings:
Example:
export AUTH_PROJECT_PATH="/home/erik997/dev/python/aa/allianceauth-dev/myauth"
export DJANGO_SETTINGS_MODULE="myauth.settings.local"
Careful: This script will utilize all existing Discord users and make changes!
"""
# start django project
import os
import sys
if not 'AUTH_PROJECT_PATH' in os.environ:
print('AUTH_PROJECT_PATH is not set')
exit(1)
if not 'DJANGO_SETTINGS_MODULE' in os.environ:
print('DJANGO_SETTINGS_MODULE is not set')
exit(1)
sys.path.insert(0, os.environ['AUTH_PROJECT_PATH'])
import django
django.setup()
# normal imports
import logging
from uuid import uuid1
import random
from django.core.cache import caches
from django.contrib.auth.models import User, Group
from allianceauth.services.modules.discord.models import DiscordUser
logger = logging.getLogger('allianceauth')
MAX_RUNS = 3
def clear_cache():
default_cache = caches['default']
redis = default_cache.get_master_client()
redis.flushall()
logger.info('Cache flushed')
def run_many_updates(runs):
logger.info('Starting piloting_tasks for %d runs', runs)
users = list()
all_groups = Group.objects.all()
for i in range(runs):
if not users:
users = list(User.objects.filter(discord__isnull=False))
user = users.pop()
logger.info('%d/%d: Starting run with user %s', i + 1, runs, user)
# force change of nick
new_nick = f'Testnick {uuid1().hex}'[:32]
logger.info(
'%d/%d: Changing nickname of %s to "%s"', i + 1, runs, user, new_nick
)
user.profile.main_character.character_name = new_nick
user.profile.main_character.save()
# force change of groups
user_groups = user.groups.all()
user.groups.remove(random.choice(user_groups))
while True:
new_group = random.choice(all_groups)
if new_group not in user_groups:
break
logger.info('%d/%d: Adding group "%s" to user %s', i + 1, runs, new_group, user)
user.groups.add(new_group)
logger.info('All %d runs have been started', runs)
if __name__ == "__main__":
clear_cache()
run_many_updates(MAX_RUNS)

View File

@@ -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)

View File

@@ -3,11 +3,13 @@ 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
from ..auth_hooks import DiscordService
from ..models import DiscordUser, DiscordClient
from ..discord_client import DiscordClient
from ..models import DiscordUser
from ..utils import set_logger_to_file
@@ -29,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()
@@ -88,16 +91,17 @@ class TestDiscordService(TestCase):
service = self.service()
service.sync_nicknames_bulk([self.member])
self.assertTrue(mock_update_nicknames_bulk.delay.called)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
def test_delete_user_is_member(self, mock_DiscordClient):
def test_delete_user_is_member(self, mock_DiscordClient):
mock_DiscordClient.return_value.remove_guild_member.return_value = True
service = self.service()
service.delete_user(self.member)
service = self.service()
service.delete_user(self.member, notify_user=True)
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
self.assertTrue(Notification.objects.filter(user=self.member).exists())
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
def test_delete_user_is_not_member(self, mock_DiscordClient):

View File

@@ -1,22 +1,441 @@
"""Integration tests
Testing all components of the service, with the exception of the Discord API.
Please note that these tests require Redis and will flush it
"""
from collections import namedtuple
import logging
from unittest.mock import patch, Mock
from uuid import uuid1
from django_webtest import WebTest
from unittest.mock import patch
from requests.exceptions import HTTPError
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, 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 (
add_permissions_to_members,
MODULE_PATH,
TEST_USER_NAME,
TEST_MAIN_NAME,
TEST_MAIN_ID
TEST_GUILD_ID,
TEST_USER_NAME,
TEST_USER_ID,
TEST_USER_DISCRIMINATOR,
TEST_MAIN_NAME,
TEST_MAIN_ID,
MODULE_PATH,
add_permissions_to_members,
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE,
ROLE_MIKE,
create_role,
create_user_info
)
from ..discord_client.app_settings import DISCORD_API_BASE_URL
from ..models import DiscordUser
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'])
user_get_current_request = DiscordRequest(
method='GET',
url=f'{DISCORD_API_BASE_URL}users/@me'
)
guild_infos_request = DiscordRequest(
method='GET',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}'
)
guild_roles_request = DiscordRequest(
method='GET',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/roles'
)
create_guild_role_request = DiscordRequest(
method='POST',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/roles'
)
guild_member_request = DiscordRequest(
method='GET',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/{TEST_USER_ID}'
)
add_guild_member_request = DiscordRequest(
method='PUT',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/{TEST_USER_ID}'
)
modify_guild_member_request = DiscordRequest(
method='PATCH',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/{TEST_USER_ID}'
)
remove_guild_member_request = DiscordRequest(
method='DELETE',
url=f'{DISCORD_API_BASE_URL}guilds/{TEST_GUILD_ID}/members/{TEST_USER_ID}'
)
class TestServiceUserActivation(WebTest):
def clear_cache():
default_cache = caches['default']
redis = default_cache.get_master_client()
redis.flushall()
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()
class TestServiceFeatures(TransactionTestCase):
@classmethod
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()
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,
corp_id='2',
corp_name='test_corp',
corp_ticker='TEST',
disconnect_signals=True
)
self.member_state.member_characters.add(self.main)
# 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()
# verify Discord nick was updates
nick_updated = False
for r in requests_mocker.request_history:
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))
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}
)
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))
def test_when_name_of_main_changes_and_and_rate_limited_then_dont_call_api(
self, requests_mocker
):
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
# exhausting rate limit
client = DiscordUser.objects._bot_client()
client._redis.set(
name=client._KEY_GLOBAL_RATE_LIMIT_REMAINING,
value=0,
px=2000
)
# 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()
# should not have called the API
requests_made = [
DiscordRequest(r.method, r.url) for r in requests_mocker.request_history
]
self.assertListEqual(requests_made, list())
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)
# 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)
# 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()
# 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': ['13', '99']
}
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_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_charlie)
# 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, 99})
break
self.assertTrue(roles_updated)
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
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': ['13', '99']
}
)
requests_mocker.get(
guild_roles_request.url,
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_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_charlie)
self.user.refresh_from_db()
# 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, 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()
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,
@@ -25,15 +444,28 @@ class TestServiceUserActivation(WebTest):
disconnect_signals=True
)
add_permissions_to_members()
@patch(MODULE_PATH + '.views.messages')
@patch(MODULE_PATH + '.models.DiscordUser.objects.add_user')
@patch(MODULE_PATH + '.views.messages')
@patch(MODULE_PATH + '.managers.OAuth2Session')
def test_user_activation(
self, mock_OAuth2Session, mock_add_user, mock_messages
def test_user_activation_normal(
self, requests_mocker, mock_OAuth2Session, mock_messages
):
authentication_code = 'auth_code'
mock_add_user.return_value = True
# user_get_current()
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'
oauth_url = 'https://www.example.com/oauth'
state = ''
mock_OAuth2Session.return_value.authorization_url.return_value = \
@@ -54,9 +486,166 @@ class TestServiceUserActivation(WebTest):
response = self.app.get(
reverse('discord:callback'), params={'code': authentication_code}
)
# user got a success message
self.assertTrue(mock_messages.success.called)
self.assertFalse(mock_messages.error.called)
requests_made = list()
for r in requests_mocker.request_history:
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [
user_get_current_request, guild_roles_request, add_guild_member_request
]
self.assertListEqual(requests_made, expected)
# user was added to Discord
self.assertTrue(mock_add_user.called)
@patch(MODULE_PATH + '.views.messages')
@patch(MODULE_PATH + '.managers.OAuth2Session')
def test_user_activation_failed(
self, requests_mocker, mock_OAuth2Session, mock_messages
):
# user_get_current()
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
requests_mocker.put(add_guild_member_request.url, exc=mock_exception)
authentication_code = 'auth_code'
oauth_url = 'https://www.example.com/oauth'
state = ''
mock_OAuth2Session.return_value.authorization_url.return_value = \
oauth_url, state
# login
self.app.set_user(self.member)
# click activate on the service page
response = self.app.get(reverse('discord:activate'))
# check we got a redirect to Discord OAuth
self.assertRedirects(
response, expected_url=oauth_url, fetch_redirect_response=False
)
# simulate Discord callback
response = self.app.get(
reverse('discord:callback'), params={'code': authentication_code}
)
# user got a success message
self.assertFalse(mock_messages.success.called)
self.assertTrue(mock_messages.error.called)
requests_made = list()
for r in requests_mocker.request_history:
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [
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()
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'})
# remove_guild_member()
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'))
# check we got a redirect to service page
self.assertRedirects(response, expected_url=reverse('services:services'))
# user got a success message
self.assertTrue(mock_messages.success.called)
self.assertFalse(mock_messages.error.called)
requests_made = list()
for r in requests_mocker.request_history:
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [remove_guild_member_request, guild_infos_request]
self.assertListEqual(requests_made, expected)
@patch(MODULE_PATH + '.views.messages')
def test_user_deactivation_fails(self, requests_mocker, mock_messages):
# guild_infos()
requests_mocker.get(
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'})
# remove_guild_member()
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'))
# check we got a redirect to service page
self.assertRedirects(response, expected_url=reverse('services:services'))
# user got a success message
self.assertFalse(mock_messages.success.called)
self.assertTrue(mock_messages.error.called)
requests_made = list()
for r in requests_mocker.request_history:
obj = DiscordRequest(r.method, r.url)
requests_made.append(obj)
expected = [remove_guild_member_request, guild_infos_request]
self.assertListEqual(requests_made, expected)
@patch(MODULE_PATH + '.views.messages')
def test_user_add_new_server(self, requests_mocker, mock_messages):
# guild_infos()
mock_exception = HTTPError('can not get guild info from Discord API')
mock_exception.response = Mock()
mock_exception.response.status_code = 440
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'))

View File

@@ -14,8 +14,12 @@ from . import (
TEST_USER_ID,
TEST_MAIN_NAME,
TEST_MAIN_ID,
MODULE_PATH
MODULE_PATH,
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE
)
from ..discord_client.tests import create_matched_role
from ..app_settings import (
DISCORD_APP_ID,
DISCORD_APP_SECRET,
@@ -32,7 +36,6 @@ logger = set_logger_to_file(MODULE_PATH + '.managers', __file__)
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
@patch(MODULE_PATH + '.models.DiscordUser.objects._exchange_auth_code_for_token')
@patch(MODULE_PATH + '.models.DiscordUser.objects.model._guild_get_or_create_role_ids')
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_group_names')
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_formatted_nick')
class TestAddUser(TestCase):
@@ -50,16 +53,16 @@ class TestAddUser(TestCase):
def test_can_create_user_no_roles_no_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -79,16 +82,20 @@ class TestAddUser(TestCase):
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
role_ids = [1, 2, 3]
roles = [
create_matched_role(ROLE_ALPHA),
create_matched_role(ROLE_BRAVO),
create_matched_role(ROLE_CHARLIE)
]
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = ['a', 'b', 'c']
mock_guild_get_or_create_role_ids.return_value = role_ids
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = roles
mock_DiscordClient.return_value.add_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -101,7 +108,7 @@ class TestAddUser(TestCase):
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertEqual(kwargs['access_token'], self.access_token)
self.assertEqual(kwargs['role_ids'], role_ids)
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
@@ -109,15 +116,15 @@ class TestAddUser(TestCase):
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -137,16 +144,16 @@ class TestAddUser(TestCase):
def test_can_create_user_no_roles_and_without_nick_if_turned_off(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -165,16 +172,16 @@ class TestAddUser(TestCase):
def test_can_activate_existing_guild_member(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -187,16 +194,16 @@ class TestAddUser(TestCase):
def test_return_false_when_user_creation_fails(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = False
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
@@ -209,16 +216,16 @@ class TestAddUser(TestCase):
def test_return_false_when_on_api_backoff(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = None
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.side_effect = \
DiscordApiBackoff(999)
@@ -232,16 +239,16 @@ class TestAddUser(TestCase):
def test_return_false_on_http_error(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = None
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_exception = HTTPError('error')
mock_exception.response = Mock()
mock_exception.response.status_code = 500
@@ -354,3 +361,26 @@ 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)
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_DiscordClient):
server_name = "El Dorado"
mock_DiscordClient.return_value.guild_name.return_value = server_name
self.assertEqual(DiscordUser.objects.server_name(), server_name)
def test_returns_empty_string_when_api_throws_http_error(self, 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(), "")

View File

@@ -6,8 +6,19 @@ from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from . import TEST_USER_NAME, TEST_USER_ID, TEST_MAIN_NAME, TEST_MAIN_ID, MODULE_PATH
from . import (
TEST_USER_NAME,
TEST_USER_ID,
TEST_MAIN_NAME,
TEST_MAIN_ID,
MODULE_PATH,
ROLE_ALPHA,
ROLE_BRAVO,
ROLE_CHARLIE,
ROLE_MIKE
)
from ..discord_client import DiscordClient, DiscordApiBackoff
from ..discord_client.tests import create_matched_role
from ..models import DiscordUser
from ..utils import set_logger_to_file
@@ -28,36 +39,16 @@ class TestBasicsAndHelpers(TestCase):
discord_user = DiscordUser.objects.create(user=user, uid=TEST_USER_ID)
expected = 'DiscordUser(user=\'Peter Parker\', uid=198765432012345678)'
self.assertEqual(repr(discord_user), expected)
def test_guild_get_or_create_role_ids(self):
mock_client = Mock(spec=DiscordClient)
mock_client.match_guild_roles_to_names.return_value = \
[({'id': 1, 'name': 'alpha'}, True), ({'id': 2, 'name': 'bravo'}, True)]
result = DiscordUser._guild_get_or_create_role_ids(mock_client, [])
excepted = [1, 2]
self.assertEqual(set(result), set(excepted))
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
class TestUpdateNick(TestCase):
def setUp(self):
def setUp(self):
self.user = AuthUtils.create_user(TEST_USER_NAME)
self.discord_user = DiscordUser.objects.create(
user=self.user, uid=TEST_USER_ID
)
@staticmethod
def user_info(nick):
return {
'user': {
'id': TEST_USER_ID,
'username': TEST_USER_NAME
},
'nick': nick,
'roles': [1, 2, 3]
}
def test_can_update(self, mock_DiscordClient):
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
@@ -74,9 +65,7 @@ class TestUpdateNick(TestCase):
self.assertFalse(result)
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
def test_return_none_if_user_no_longer_a_member(
self, mock_DiscordClient
):
def test_return_none_if_user_no_longer_a_member(self, mock_DiscordClient):
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
mock_DiscordClient.return_value.modify_guild_member.return_value = None
@@ -93,12 +82,94 @@ class TestUpdateNick(TestCase):
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
class TestUpdateUsername(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = AuthUtils.create_user(TEST_USER_NAME)
def setUp(self):
self.discord_user = DiscordUser.objects.create(
user=self.user,
uid=TEST_USER_ID,
username=TEST_MAIN_NAME,
discriminator='1234'
)
def test_can_update(self, mock_DiscordClient):
new_username = 'New name'
new_discriminator = '9876'
user_info = {
'user': {
'id': str(TEST_USER_ID),
'username': new_username,
'discriminator': new_discriminator,
}
}
mock_DiscordClient.return_value.guild_member.return_value = user_info
result = self.discord_user.update_username()
self.assertTrue(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
self.discord_user.refresh_from_db()
self.assertEqual(self.discord_user.username, new_username)
self.assertEqual(self.discord_user.discriminator, new_discriminator)
def test_return_none_if_user_no_longer_a_member(self, mock_DiscordClient):
mock_DiscordClient.return_value.guild_member.return_value = None
result = self.discord_user.update_username()
self.assertIsNone(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
def test_return_false_if_api_returns_false(self, mock_DiscordClient):
mock_DiscordClient.return_value.guild_member.return_value = False
result = self.discord_user.update_username()
self.assertFalse(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
def test_return_false_if_api_returns_corrput_data_1(self, mock_DiscordClient):
mock_DiscordClient.return_value.guild_member.return_value = {'invalid': True}
result = self.discord_user.update_username()
self.assertFalse(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
def test_return_false_if_api_returns_corrput_data_2(self, mock_DiscordClient):
user_info = {
'user': {
'id': str(TEST_USER_ID),
'discriminator': '1234',
}
}
mock_DiscordClient.return_value.guild_member.return_value = user_info
result = self.discord_user.update_username()
self.assertFalse(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
def test_return_false_if_api_returns_corrput_data_3(self, mock_DiscordClient):
user_info = {
'user': {
'id': str(TEST_USER_ID),
'username': TEST_USER_NAME,
}
}
mock_DiscordClient.return_value.guild_member.return_value = user_info
result = self.discord_user.update_username()
self.assertFalse(result)
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
@patch(MODULE_PATH + '.models.notify')
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
class TestDeleteUser(TestCase):
def setUp(self):
self.user = AuthUtils.create_user(TEST_USER_NAME)
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = AuthUtils.create_user(TEST_USER_NAME)
def setUp(self):
self.discord_user = DiscordUser.objects.create(
user=self.user, uid=TEST_USER_ID
)
@@ -148,25 +219,48 @@ class TestDeleteUser(TestCase):
)
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
self.assertFalse(mock_notify.called)
def test_return_false_on_api_backoff(self, mock_DiscordClient, mock_notify):
def test_raise_exception_on_api_backoff(
self, mock_DiscordClient, mock_notify
):
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
DiscordApiBackoff(999)
result = self.discord_user.delete_user()
with self.assertRaises(DiscordApiBackoff):
self.discord_user.delete_user()
def test_return_false_on_api_backoff_and_exception_handling_on(
self, mock_DiscordClient, mock_notify
):
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
DiscordApiBackoff(999)
result = self.discord_user.delete_user(handle_api_exceptions=True)
self.assertFalse(result)
def test_return_false_on_http_error(self, mock_DiscordClient, mock_notify):
def test_raise_exception_on_http_error(
self, mock_DiscordClient, mock_notify
):
mock_exception = HTTPError('error')
mock_exception.response = Mock()
mock_exception.response.status_code = 500
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
mock_exception
result = self.discord_user.delete_user()
with self.assertRaises(HTTPError):
self.discord_user.delete_user()
def test_return_false_on_http_error_and_exception_handling_on(
self, mock_DiscordClient, mock_notify
):
mock_exception = HTTPError('error')
mock_exception.response = Mock()
mock_exception.response.status_code = 500
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
mock_exception
result = self.discord_user.delete_user(handle_api_exceptions=True)
self.assertFalse(result)
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
@patch(MODULE_PATH + '.models.DiscordUser._guild_get_or_create_role_ids')
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_group_names')
class TestUpdateGroups(TestCase):
@@ -175,48 +269,169 @@ class TestUpdateGroups(TestCase):
self.discord_user = DiscordUser.objects.create(
user=self.user, uid=TEST_USER_ID
)
self.guild_roles = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
self.roles_requested = [
create_matched_role(ROLE_ALPHA), create_matched_role(ROLE_BRAVO)
]
def test_can_update(
def test_update_if_needed(
self,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_DiscordClient
):
roles_requested = [1, 2, 3]
):
roles_current = [1]
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = roles_requested
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = self.discord_user.update_groups()
self.assertTrue(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(set(kwargs['role_ids']), {1, 2})
def test_update_if_needed_and_preserve_managed_roles(
self,
mock_user_group_names,
mock_DiscordClient
):
roles_current = [1, 13]
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = self.discord_user.update_groups()
self.assertTrue(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(set(kwargs['role_ids']), {1, 2, 13})
def test_dont_update_if_not_needed(
self,
mock_user_group_names,
mock_DiscordClient
):
roles_current = [1, 2, 13]
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
result = self.discord_user.update_groups()
self.assertTrue(result)
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
def test_update_if_user_has_no_roles_on_discord(
self,
mock_user_group_names,
mock_DiscordClient
):
roles_current = []
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = self.discord_user.update_groups()
self.assertTrue(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(set(kwargs['role_ids']), {1, 2})
def test_return_none_if_user_no_longer_a_member(
self,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
self,
mock_user_group_names,
mock_DiscordClient
):
roles_requested = [1, 2, 3]
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = roles_requested
mock_DiscordClient.return_value.modify_guild_member.return_value = None
):
mock_DiscordClient.return_value.guild_member.return_value = None
result = self.discord_user.update_groups()
self.assertIsNone(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
def test_return_false_if_api_returns_false(
self,
mock_user_group_names,
mock_guild_get_or_create_role_ids,
mock_user_group_names,
mock_DiscordClient
):
roles_requested = [1, 2, 3]
roles_current = [1]
mock_user_group_names.return_value = []
mock_guild_get_or_create_role_ids.return_value = roles_requested
mock_DiscordClient.return_value.modify_guild_member.return_value = False
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = False
result = self.discord_user.update_groups()
self.assertFalse(result)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_raise_exception_if_member_has_unknown_roles(
self,
mock_user_group_names,
mock_DiscordClient
):
roles_current = [99]
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
with self.assertRaises(RuntimeError):
self.discord_user.update_groups()
def test_refresh_guild_roles_user_roles_dont_not_match(
self,
mock_user_group_names,
mock_DiscordClient
):
def my_guild_roles(guild_id, use_cache=True):
if use_cache:
return [ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE]
else:
return [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
roles_current = [3]
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.side_effect = my_guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'roles': roles_current}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = self.discord_user.update_groups()
self.assertTrue(result)
self.assertEqual(mock_DiscordClient.return_value.guild_roles.call_count, 2)
def test_raise_exception_if_member_info_is_invalid(
self,
mock_user_group_names,
mock_DiscordClient
):
mock_user_group_names.return_value = []
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = self.roles_requested
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
mock_DiscordClient.return_value.guild_member.return_value = \
{'user': 'dummy'}
mock_DiscordClient.return_value.modify_guild_member.return_value = True
with self.assertRaises(RuntimeError):
self.discord_user.update_groups()

View File

@@ -150,13 +150,29 @@ class TestUpdateNickname(TestCase):
update_nickname_inner(mock_task, self.user.pk)
@patch(MODULE_PATH + '.DiscordUser.update_username')
class TestUpdateUsername(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = AuthUtils.create_member(TEST_USER_NAME)
cls.discord_user = DiscordUser.objects.create(user=cls.user, uid=TEST_USER_ID)
def test_can_update_username(self, mock_update_username):
mock_update_username.return_value = True
tasks.update_username(self.user.pk)
self.assertTrue(mock_update_username.called)
@patch(MODULE_PATH + '.DiscordUser.delete_user')
class TestDeleteUser(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = AuthUtils.create_member('Peter Parker')
cls.user = AuthUtils.create_member(TEST_USER_NAME)
cls.discord_user = DiscordUser.objects.create(user=cls.user, uid=TEST_USER_ID)
def test_can_delete_user(self, mock_delete_user):
@@ -270,11 +286,37 @@ class TestBulkTasks(TestCase):
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))
@patch(MODULE_PATH + '.update_username.si')
def test_can_update_username_for_multiple_users(self, 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)
DiscordUser.objects.create(user=self.user_3, uid=789)
expected_pks = [du_1.pk, du_2.pk]
tasks.update_usernames_bulk(expected_pks)
self.assertEqual(mock_update_username.call_count, 2)
current_pks = [args[0][0] for args in mock_update_username.call_args_list]
self.assertSetEqual(set(current_pks), set(expected_pks))
@patch(MODULE_PATH + '.update_username.si')
def test_can_update_all_usernames(self, 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]
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))
@patch(MODULE_PATH + '.DISCORD_SYNC_NAMES', True)
@patch(MODULE_PATH + '.update_nickname')
@patch(MODULE_PATH + '.update_groups')
@patch(MODULE_PATH + '.update_username')
def test_can_update_all_incl_nicknames(
self, mock_update_groups, mock_update_nickname
self, mock_update_usernames, mock_update_groups, mock_update_nickname
):
du_1 = DiscordUser.objects.create(user=self.user_1, uid=123)
du_2 = DiscordUser.objects.create(user=self.user_2, uid=456)
@@ -291,11 +333,17 @@ class TestBulkTasks(TestCase):
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))
self.assertEqual(mock_update_usernames.si.call_count, 3)
current_pks = [args[0][0] for args in mock_update_usernames.si.call_args_list]
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))
@patch(MODULE_PATH + '.DISCORD_SYNC_NAMES', False)
@patch(MODULE_PATH + '.update_nickname')
@patch(MODULE_PATH + '.update_groups')
@patch(MODULE_PATH + '.update_username')
def test_can_update_all_excl_nicknames(
self, mock_update_groups, mock_update_nickname
self, mock_update_usernames, mock_update_groups, mock_update_nickname
):
du_1 = DiscordUser.objects.create(user=self.user_1, uid=123)
du_2 = DiscordUser.objects.create(user=self.user_2, uid=456)
@@ -308,3 +356,8 @@ class TestBulkTasks(TestCase):
self.assertSetEqual(set(current_pks), set(expected_pks))
self.assertEqual(mock_update_nickname.si.call_count, 0)
self.assertEqual(mock_update_usernames.si.call_count, 3)
current_pks = [args[0][0] for args in mock_update_usernames.si.call_args_list]
expected_pks = [du_1.pk, du_2.pk, du_3.pk]
self.assertSetEqual(set(current_pks), set(expected_pks))

View File

@@ -7,7 +7,8 @@ from django.urls import reverse
from allianceauth.tests.auth_utils import AuthUtils
from . import MODULE_PATH, add_permissions_to_members, TEST_USER_NAME, TEST_USER_ID
from ..models import DiscordUser, DiscordClient
from ..discord_client import DiscordClient
from ..models import DiscordUser
from ..utils import set_logger_to_file
from ..views import (
discord_callback,

View File

@@ -23,7 +23,9 @@ ACCESS_PERM = 'discord.access_discord'
@permission_required(ACCESS_PERM)
def deactivate_discord(request):
logger.debug("deactivate_discord called by user %s", request.user)
if request.user.discord.delete_user(is_rate_limited=False):
if request.user.discord.delete_user(
is_rate_limited=False, handle_api_exceptions=True
):
logger.info("Successfully deactivated discord for user %s", request.user)
messages.success(request, _('Deactivated Discord account.'))
else:
@@ -40,7 +42,9 @@ def deactivate_discord(request):
@permission_required(ACCESS_PERM)
def reset_discord(request):
logger.debug("reset_discord called by user %s", request.user)
if request.user.discord.delete_user(is_rate_limited=False):
if request.user.discord.delete_user(
is_rate_limited=False, handle_api_exceptions=True
):
logger.info(
"Successfully deleted discord user for user %s - "
"forwarding to discord activation.",

View File

@@ -185,7 +185,6 @@ class DiscourseManager:
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

View File

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

View File

@@ -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'),
),
]

View File

@@ -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

View File

@@ -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):

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ from . import urls
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:'])

View File

@@ -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>

View File

@@ -20,7 +20,7 @@
</div>
<div class="text-right" style="position:absolute;bottom:5px;right:5px;">
<a href="https://gitlab.com/allianceauth/allianceauth/issues"><span class="label" style="background-color:#e65328;">
<i class="fa fa-gitlab" aria-hidden="true"></i> Powered by GitLab</span>
<i class="fab fa-gitlab" aria-hidden="true"></i> Powered by GitLab</span>
</a>
</div>
</div>
@@ -36,36 +36,28 @@
{{ current_version }}
</p>
</li>
<li class="list-group-item list-group-item-{% if latest_major %}success{% else %}warning{% endif %}">
<h5 class="list-group-item-heading">{% trans "Latest Major" %}</h5>
<li class="list-group-item list-group-item-{% if latest_patch %}success{% elif latest_minor %}warning{% else %}danger{% endif %}">
<h5 class="list-group-item-heading">{% trans "Latest Stable" %}</h5>
<p class="list-group-item-text">
<a href="https://gitlab.com/allianceauth/allianceauth/tags" style="color:#000">
<i class="fa fa-gitlab hidden-xs" aria-hidden="true"></i>
{{ latest_major_version }}
</a>
{% if not latest_major %}<br>{% trans "Update available" %}{% endif %}
</p>
</li>
<li class="list-group-item list-group-item-{% if latest_minor %}success{% else %}warning{% endif %}">
<h5 class="list-group-item-heading">{% trans "Latest Minor" %}</h5>
<p class="list-group-item-text">
<a href="https://gitlab.com/allianceauth/allianceauth/tags" style="color:#000">
<i class="fa fa-gitlab hidden-xs" aria-hidden="true"></i>
{{ latest_minor_version }}
</a>
{% if not latest_minor %}<br>{% trans "Update available" %}{% endif %}
</p>
</li>
<li class="list-group-item list-group-item-{% if latest_patch %}success{% else %}danger{% endif %}">
<h5 class="list-group-item-heading">{% trans "Latest Patch" %}</h5>
<p class="list-group-item-text">
<a href="https://gitlab.com/allianceauth/allianceauth/tags" style="color:#000">
<i class="fa fa-gitlab hidden-xs" aria-hidden="true"></i>
<i class="fab fa-gitlab hidden-xs" aria-hidden="true"></i>
{{ latest_patch_version }}
</a>
{% if not latest_patch %}<br>{% trans "Update available" %}{% endif %}
</p>
</li>
{% if latest_beta %}
<li class="list-group-item list-group-item-info">
<h5 class="list-group-item-heading">{% trans "Latest Pre-Release" %}</h5>
<p class="list-group-item-text">
<a href="https://gitlab.com/allianceauth/allianceauth/tags" style="color:#000">
<i class="fab fa-gitlab hidden-xs" aria-hidden="true"></i>
{{ latest_beta_version }}
</a>
<br>{% trans "Pre-Release available" %}
</p>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@@ -1,26 +1,28 @@
{% load i18n %}
{% load navactive %}
{% load menu_items %}
{% load groupmanagement %}
<div class="col-sm-2 auth-side-navbar" role="navigation">
<div class="collapse navbar-collapse auth-menus-collapse auth-side-navbar-collapse">
<ul class="nav nav-pills nav-stacked gray-icon-color" id="side-menu">
<li>
<a class="{% navactive request 'authentication:dashboard' %}"
href="{% url 'authentication:dashboard' %}">
<i class="fa fa-dashboard fa-fw"></i> {% trans "Dashboard" %}
<i class="fas fa-tachometer-alt fa-fw"></i> {% trans "Dashboard" %}
</a>
</li>
<li>
<a class="{% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
<i class="fa fa-cogs fa-fw fa-sitemap"></i> {% trans "Groups" %}
<i class="fas fa-sitemap fa-fw"></i> {% trans "Groups" %}
</a>
</li>
{% if can_manage_groups %}
{% if request.user|can_manage_groups %}
<li>
<a class="{% navactive request 'groupmanagement:management groupmanagement:membership groupmanagement:membership_list' %}"
href="{% url 'groupmanagement:management' %}">
<i class="fa fa-lock fa-sitemap fa-fw"></i> {% trans "Group Management" %}
<i class="fas fa-sitemap fa-fw"></i> {% trans "Group Management" %}
</a>
</li>
{% endif %}

View File

@@ -28,7 +28,7 @@
</li>
{% else %}
<li><a href="{% url 'notifications:list' %}">
<i class="fa fa-bell-o"></i></a>
<i class="far fa-bell"></i></a>
</li>
{% endif %}
{% if user.is_authenticated %}
@@ -42,7 +42,7 @@
{% if user.is_superuser %}
<li>
<a class="navbar-brand" style="margin-left: -4px;" href="https://allianceauth.readthedocs.io/" target="_blank">
<i class="fa fa-question-circle fa-fw"></i>
<i class="fas fa-question-circle fa-fw"></i>
</a>
</li>
{% endif %}

View File

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

View File

@@ -1,60 +1,59 @@
import requests
import logging
import requests
import amqp.exceptions
import semantic_version as semver
from packaging.version import Version as Pep440Version, InvalidVersion
from celery.app import app_or_default
from django import template
from django.conf import settings
from django.core.cache import cache
from celery.app import app_or_default
from allianceauth import __version__
register = template.Library()
TAG_CACHE_TIME = 10800 # 3 hours
# cache timers
TAG_CACHE_TIME = 3600 # 1 hours
NOTIFICATION_CACHE_TIME = 300 # 5 minutes
# timeout for all requests
REQUESTS_TIMEOUT = 5 # 5 seconds
# max pages to be fetched from gitlab
MAX_PAGES = 50
GITLAB_AUTH_REPOSITORY_TAGS_URL = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/repository/tags'
)
GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement'
)
logger = logging.getLogger(__name__)
def get_git_tags():
request = requests.get('https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/repository/tags')
request.raise_for_status()
return request.json()
def get_notification_issues():
# notification
request = requests.get(
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues?labels=announcement')
request.raise_for_status()
return request.json()
@register.inclusion_tag('allianceauth/admin-status/overview.html', takes_context=True)
def status_overview(context):
response = {
'notifications': list(),
'latest_major': True,
'latest_minor': True,
'latest_patch': True,
'notifications': list(),
'current_version': __version__,
'task_queue_length': -1,
}
response.update(get_notifications())
response.update(get_version_info())
response.update({'task_queue_length': get_celery_queue_length()})
response.update(_current_notifications())
response.update(_current_version_summary())
response.update({'task_queue_length': _fetch_celery_queue_length()})
return response
def get_celery_queue_length():
def _fetch_celery_queue_length():
try:
app = app_or_default(None)
with app.connection_or_acquire() as conn:
return conn.default_channel.queue_declare(
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'), passive=True).message_count
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'),
passive=True
).message_count
except amqp.exceptions.ChannelError:
# Queue doesn't exist, probably empty
return 0
@@ -63,72 +62,121 @@ def get_celery_queue_length():
return -1
def get_notifications():
response = {
'notifications': list(),
}
def _current_notifications() -> dict:
"""returns the newest 5 announcement issues"""
try:
notifications = cache.get_or_set('gitlab_notification_issues', get_notification_issues,
NOTIFICATION_CACHE_TIME)
# Limit notifications to those posted by repo owners and members
response['notifications'] += notifications[:5]
notifications = cache.get_or_set(
'gitlab_notification_issues',
_fetch_notification_issues_from_gitlab,
NOTIFICATION_CACHE_TIME
)
top_notifications = notifications[:5]
except requests.RequestException:
logger.exception('Error while getting gitlab notifications')
top_notifications = []
response = {
'notifications': top_notifications,
}
return response
def get_version_info():
response = {
'latest_major': True,
'latest_minor': True,
'latest_patch': True,
'current_version': __version__,
}
def _fetch_notification_issues_from_gitlab() -> list:
return _fetch_list_from_gitlab(GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL, max_pages=10)
def _current_version_summary() -> dict:
"""returns the current version info"""
try:
tags = cache.get_or_set('git_release_tags', get_git_tags, TAG_CACHE_TIME)
current_ver = semver.Version.coerce(__version__)
# Set them all to the current version to start
# If the server has only earlier or the same version
# then this will become the major/minor/patch versions
latest_major = current_ver
latest_minor = current_ver
latest_patch = current_ver
response.update({
'latest_major_version': str(latest_major),
'latest_minor_version': str(latest_minor),
'latest_patch_version': str(latest_patch),
})
for tag in tags:
tag_name = tag.get('name')
if tag_name[0] == 'v':
# Strip 'v' off front of verison if it exists
tag_name = tag_name[1:]
try:
tag_ver = semver.Version.coerce(tag_name)
except ValueError:
tag_ver = semver.Version('0.0.0', partial=True)
if tag_ver > current_ver:
if latest_major is None or tag_ver > latest_major:
latest_major = tag_ver
response['latest_major_version'] = tag_name
if tag_ver.major > current_ver.major:
response['latest_major'] = False
elif tag_ver.major == current_ver.major:
if latest_minor is None or tag_ver > latest_minor:
latest_minor = tag_ver
response['latest_minor_version'] = tag_name
if tag_ver.minor > current_ver.minor:
response['latest_minor'] = False
elif tag_ver.minor == current_ver.minor:
if latest_patch is None or tag_ver > latest_patch:
latest_patch = tag_ver
response['latest_patch_version'] = tag_name
if tag_ver.patch > current_ver.patch:
response['latest_patch'] = False
tags = cache.get_or_set(
'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME
)
except requests.RequestException:
logger.exception('Error while getting gitlab release tags')
return {}
latest_major_version, latest_minor_version, latest_patch_version, latest_beta_version = \
_latests_versions(tags)
current_version = Pep440Version(__version__)
has_latest_major = \
current_version >= latest_major_version if latest_major_version else False
has_latest_minor = \
current_version >= latest_minor_version if latest_minor_version else False
has_latest_patch = \
current_version >= latest_patch_version if latest_patch_version else False
has_current_beta = \
current_version.base_version <= latest_beta_version.base_version \
and latest_major_version.base_version <= latest_beta_version.base_version \
if latest_beta_version else False
response = {
'latest_major': has_latest_major,
'latest_minor': has_latest_minor,
'latest_patch': has_latest_patch,
'latest_beta': has_current_beta,
'current_version': str(current_version),
'latest_major_version': str(latest_major_version),
'latest_minor_version': str(latest_minor_version),
'latest_patch_version': str(latest_patch_version),
'latest_beta_version': str(latest_beta_version)
}
return response
def _fetch_tags_from_gitlab():
return _fetch_list_from_gitlab(GITLAB_AUTH_REPOSITORY_TAGS_URL)
def _latests_versions(tags: list) -> tuple:
"""returns latests version from given tags list
Non-compliant tags will be ignored
"""
versions = list()
betas = list()
for tag in tags:
try:
version = Pep440Version(tag.get('name'))
except InvalidVersion:
pass
else:
if version.is_prerelease or version.is_devrelease:
betas.append(version)
else:
versions.append(version)
latest_version = latest_patch_version = max(versions)
latest_major_version = min([
v for v in versions if v.major == latest_version.major
])
latest_minor_version = min([
v for v in versions
if v.major == latest_version.major and v.minor == latest_version.minor
])
latest_beta_version = max(betas)
return latest_major_version, latest_minor_version, latest_patch_version, latest_beta_version
def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES):
"""returns a list from the GitLab API. Supports pageing"""
result = list()
for page in range(1, max_pages + 1):
request = requests.get(
url, params={'page': page}, timeout=REQUESTS_TIMEOUT
)
request.raise_for_status()
result += request.json()
if 'x-total-pages' in request.headers:
try:
total_pages = int(request.headers['x-total-pages'])
except ValueError:
total_pages = None
else:
total_pages = None
if not total_pages or page >= total_pages:
break
return result

View File

@@ -141,12 +141,18 @@ class AuthUtils:
post_save.connect(check_state_on_character_update, sender=EveCharacter)
@classmethod
def add_main_character(cls, user, name, character_id, corp_id='', corp_name='', corp_ticker='', alliance_id='',
def add_main_character(cls, user, name, character_id, corp_id=2345, corp_name='', corp_ticker='', alliance_id=None,
alliance_name=''):
if alliance_id:
try:
alliance_id = int(alliance_id)
except:
alliance_id = None
char = EveCharacter.objects.create(
character_id=character_id,
character_id=int(character_id),
character_name=name,
corporation_id=corp_id,
corporation_id=int(corp_id),
corporation_name=corp_name,
corporation_ticker=corp_ticker,
alliance_id=alliance_id,
@@ -160,10 +166,10 @@ class AuthUtils:
user,
name,
character_id,
corp_id='',
corp_id=2345,
corp_name='',
corp_ticker='',
alliance_id='',
alliance_id=None,
alliance_name='',
disconnect_signals=False
):
@@ -171,10 +177,16 @@ class AuthUtils:
if disconnect_signals:
cls.disconnect_signals()
if alliance_id:
try:
alliance_id = int(alliance_id)
except:
alliance_id = None
char = EveCharacter.objects.create(
character_id=character_id,
character_id=int(character_id),
character_name=name,
corporation_id=corp_id,
corporation_id=int(corp_id),
corporation_name=corp_name,
corporation_ticker=corp_ticker,
alliance_id=alliance_id,

View File

@@ -7,7 +7,7 @@ from . import urls
class TimerboardMenu(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self, 'Structure Timers',
'fa fa-clock-o fa-fw',
'far fa-clock fa-fw',
'timerboard:view',
navactive=['timerboard:'])

BIN
allianceauth_model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

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

View File

@@ -12,4 +12,22 @@ logger = get_extension_logger(__name__)
```
This works by creating a child logger of the extension logger which propagates all log entries
to the parent (extensions) logger.
to the parent (extensions) logger.
## Changing the Logging Level
By default, the extension logger's level is set to `DEBUG`.
To change this, uncomment (or add) the following line in `local.py`.
```python
LOGGING['handlers']['extension_file']['level'] = 'INFO'
```
*(Remember to restart your supervisor workers after changes to `local.py`)*
This will change the logger's level to the level you define.
Options are: *(all options accept entries of levels listed below them)*
* `DEBUG`
* `INFO`
* `WARNING`
* `ERROR`
* `CRITICAL`

View File

@@ -6,6 +6,13 @@ This package generates profile URLs for eve entities on 3rd party websites like
Location: ``allianceauth.eveonline.evelinks``
eveimageserver
===============
.. automodule:: allianceauth.eveonline.evelinks.eveimageserver
:members:
:undoc-members:
dotlan
===============

View File

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

View File

@@ -10,19 +10,29 @@ Discord is very popular amongst ad-hoc small groups and larger organizations see
### Prepare Your Settings File
In your auth project's settings file, do the following:
Make the following changes in your auth project's settings file (`local.py`):
- Add `'allianceauth.services.modules.discord',` to your `INSTALLED_APPS` list
- Add `'allianceauth.services.modules.discord',` to `INSTALLED_APPS`
- Append the following to the bottom of the settings file:
```python
# Discord Configuration
DISCORD_GUILD_ID = ''
DISCORD_CALLBACK_URL = ''
DISCORD_APP_ID = ''
DISCORD_APP_SECRET = ''
DISCORD_BOT_TOKEN = ''
DISCORD_SYNC_NAMES = False
# Discord Configuration
DISCORD_GUILD_ID = ''
DISCORD_CALLBACK_URL = ''
DISCORD_APP_ID = ''
DISCORD_APP_SECRET = ''
DISCORD_BOT_TOKEN = ''
DISCORD_SYNC_NAMES = False
CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
'task': 'discord.update_all_usernames',
'schedule': crontab(hour='*/12'),
}
```
```eval_rst
.. note::
You will have to add most the values for these settings, e.g. your Discord server ID (aka guild ID), later in the setup process.
```
### Creating a Server
@@ -82,24 +92,63 @@ If you want users to have their Discord nickname changed to their in-game charac
Once users link their accounts youll notice Roles get populated on Discord. These are the equivalent to groups on every other service. The default permissions should be enough for members to use text and audio communications. Add more permissions to the roles as desired through the server management window.
## Tasks
The Discord service contains a number of tasks that can be run to manually perform updates to all users.
You can run any of these tasks from the command line. Please make sure that you are in your venv and then you can run this command from the same folder that your manage.py is located:
```bash
celery -A myauth call discord.update_all_groups
```
```eval_rst
======================== ====================================================
Name Description
======================== ====================================================
`update_all_groups` Updates groups of all users
`update_all_nicknames` Update nicknames of all users (also needs setting)
`update_all_usernames` Update locally stored Discord usernames of all users
`update_all` Update groups, nicknames, usernames of all users
======================== ====================================================
```
```eval_rst
.. note::
Depending on how many users you have, running these tasks can take considerable time to finish. You can calculate roughly 1 sec per user for all tasks, except update_all, which needs roughly 3 secs per user.
```
## Settings
You can configure your Discord services with the following settings:
Name | Description | Default
-- | -- | --
`DISCORD_APP_ID` | Oauth client ID for the Discord Auth app | `''`
`DISCORD_APP_SECRET` | Oauth client secret for the Discord Auth app | `''`
`DISCORD_BOT_TOKEN` | Generated bot token for the Discord Auth app | `''`
`DISCORD_CALLBACK_URL` | Oauth callback URL | `''`
`DISCORD_GUILD_ID` | Discord ID of your Discord server | `''`
`DISCORD_ROLES_CACHE_MAX_AGE` | How long roles retrieved from the Discord server are caches locally in milliseconds | `7200000`
`DISCORD_SYNC_NAMES` | When set to True the nicknames of Discord users will automatically be set to the user's main character name when a new user joins Discord | `False`
`DISCORD_TASKS_RETRY_PAUSE` | Pause in seconds until next retry for tasks after an error occurred | `60`
`DISCORD_TASKS_MAX_RETRIES` | max retries of tasks after an error occurred | `3`
```eval_rst
=================================== ============================================================================================= =======
Name Description Default
=================================== ============================================================================================= =======
`DISCORD_APP_ID` Oauth client ID for the Discord Auth app `''`
`DISCORD_APP_SECRET` Oauth client secret for the Discord Auth app `''`
`DISCORD_BOT_TOKEN` Generated bot token for the Discord Auth app `''`
`DISCORD_CALLBACK_URL` Oauth callback URL `''`
`DISCORD_GUILD_ID` Discord ID of your Discord server `''`
`DISCORD_GUILD_NAME_CACHE_MAX_AGE` How long the Discord server name is cached locally in milliseconds `3600000`
`DISCORD_ROLES_CACHE_MAX_AGE` How long roles retrieved from the Discord server are cached locally in milliseconds `3600000`
`DISCORD_SYNC_NAMES` When set to True the nicknames of Discord users will be set to the user's main character name `False`
`DISCORD_TASKS_RETRY_PAUSE` Pause in seconds until next retry for tasks after an error occurred `60`
`DISCORD_TASKS_MAX_RETRIES` max retries of tasks after an error occurred `3`
=================================== ============================================================================================= =======
```
## Troubleshooting
### "Unknown Error" on Discord site when activating service
This indicates your callback URL doesn't match. Ensure the `DISCORD_CALLBACK_URL` setting exactly matches the URL entered on the Discord developers site. This includes http(s), trailing slash, etc.
### "Add/Remove" Errors in Discord Service
If you are recieving errors in your Notifications after verifying that your settings are all correct try the following:
- Ensure that the bot's role in Discord is at the top of the roles list. Each time you add it to your server you will need to do this again.
- Make sure that the bot is not trying to modify the Owner of the discord, as it will fail. A holding discord account added with invite link will mitigate this.
- Make sure that the bot role on discord has all needed permissions, Admin etc., remembering that these will need to be set every time you add the bot to the Discord server.

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