mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-08 08:06:20 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3874aa6fee | ||
|
|
103e9f3a11 | ||
|
|
d02c25f421 | ||
|
|
228af38a4a | ||
|
|
051a48885c | ||
|
|
6bcdc6052f | ||
|
|
af3527e64f | ||
|
|
17ef3dd07a | ||
|
|
1f165ecd2a | ||
|
|
70d1d450a9 | ||
|
|
b667892698 | ||
|
|
dc11add0e9 | ||
|
|
cb429a0b88 | ||
|
|
b51039cfc0 | ||
|
|
eadd959d95 | ||
|
|
1856e03d88 | ||
|
|
7dcfa622a3 | ||
|
|
64251b9b3c | ||
|
|
6b073dd5fc | ||
|
|
0911fabfb2 | ||
|
|
050d3f5e63 | ||
|
|
bbe3f78ad1 | ||
|
|
8204c18895 | ||
|
|
b91c788897 | ||
|
|
1d20a3029f |
@@ -36,7 +36,7 @@ Main features:
|
|||||||
|
|
||||||
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||||
|
|
||||||
- Chinese :cn:, English :us:, German :de: and Spanish :es: localization
|
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
|
||||||
|
|
||||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# This will make sure the app is always imported when
|
# This will make sure the app is always imported when
|
||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
|
|
||||||
__version__ = '2.7.4'
|
__version__ = '2.8.0a1'
|
||||||
__title__ = 'Alliance Auth'
|
__title__ = 'Alliance Auth'
|
||||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||||
NAME = '%s v%s' % (__title__, __version__)
|
NAME = '%s v%s' % (__title__, __version__)
|
||||||
|
|||||||
@@ -5,35 +5,96 @@ from .models import EveAllianceInfo
|
|||||||
from .models import EveCharacter
|
from .models import EveCharacter
|
||||||
from .models import EveCorporationInfo
|
from .models import EveCorporationInfo
|
||||||
|
|
||||||
|
from . import providers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
TASK_PRIORITY = 7
|
TASK_PRIORITY = 7
|
||||||
|
CHUNK_SIZE = 500
|
||||||
|
|
||||||
|
|
||||||
|
def chunks(lst, n):
|
||||||
|
"""Yield successive n-sized chunks from lst."""
|
||||||
|
for i in range(0, len(lst), n):
|
||||||
|
yield lst[i:i + n]
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_corp(corp_id):
|
def update_corp(corp_id):
|
||||||
|
"""Update given corporation from ESI"""
|
||||||
EveCorporationInfo.objects.update_corporation(corp_id)
|
EveCorporationInfo.objects.update_corporation(corp_id)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_alliance(alliance_id):
|
def update_alliance(alliance_id):
|
||||||
|
"""Update given alliance from ESI"""
|
||||||
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
|
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def update_character(character_id):
|
def update_character(character_id):
|
||||||
|
"""Update given character from ESI"""
|
||||||
EveCharacter.objects.update_character(character_id)
|
EveCharacter.objects.update_character(character_id)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def run_model_update():
|
def run_model_update():
|
||||||
|
"""Update all alliances, corporations and characters from ESI"""
|
||||||
|
|
||||||
# update existing corp models
|
# update existing corp models
|
||||||
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
|
||||||
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
|
update_corp.apply_async(
|
||||||
|
args=[corp['corporation_id']], priority=TASK_PRIORITY
|
||||||
|
)
|
||||||
|
|
||||||
# update existing alliance models
|
# update existing alliance models
|
||||||
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
|
||||||
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
|
update_alliance.apply_async(
|
||||||
|
args=[alliance['alliance_id']], priority=TASK_PRIORITY
|
||||||
|
)
|
||||||
|
|
||||||
# update existing character models
|
# update existing character models
|
||||||
for character in EveCharacter.objects.all().values('character_id'):
|
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
|
||||||
update_character.apply_async(args=[character['character_id']], priority=TASK_PRIORITY)
|
for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
|
||||||
|
affiliations_raw = providers.provider.client.Character\
|
||||||
|
.post_characters_affiliation(characters=character_ids_chunk).result()
|
||||||
|
character_names = providers.provider.client.Universe\
|
||||||
|
.post_universe_names(ids=character_ids_chunk).result()
|
||||||
|
|
||||||
|
affiliations = {
|
||||||
|
affiliation.get('character_id'): affiliation
|
||||||
|
for affiliation in affiliations_raw
|
||||||
|
}
|
||||||
|
# add character names to affiliations
|
||||||
|
for character in character_names:
|
||||||
|
character_id = character.get('id')
|
||||||
|
if character_id in affiliations:
|
||||||
|
affiliations[character_id]['name'] = character.get('name')
|
||||||
|
|
||||||
|
# fetch current characters
|
||||||
|
characters = EveCharacter.objects.filter(character_id__in=character_ids_chunk)\
|
||||||
|
.values('character_id', 'corporation_id', 'alliance_id', 'character_name')
|
||||||
|
|
||||||
|
for character in characters:
|
||||||
|
character_id = character.get('character_id')
|
||||||
|
if character_id in affiliations:
|
||||||
|
affiliation = affiliations[character_id]
|
||||||
|
|
||||||
|
corp_changed = (
|
||||||
|
character.get('corporation_id') != affiliation.get('corporation_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
alliance_id = character.get('alliance_id')
|
||||||
|
if not alliance_id:
|
||||||
|
alliance_id = None
|
||||||
|
alliance_changed = alliance_id != affiliation.get('alliance_id')
|
||||||
|
|
||||||
|
name_changed = False
|
||||||
|
fetched_name = affiliation.get('name', False)
|
||||||
|
if fetched_name:
|
||||||
|
name_changed = character.get('character_name') != fetched_name
|
||||||
|
|
||||||
|
if corp_changed or alliance_changed or name_changed:
|
||||||
|
update_character.apply_async(
|
||||||
|
args=[character.get('character_id')], priority=TASK_PRIORITY
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
@@ -44,15 +44,17 @@ class TestTasks(TestCase):
|
|||||||
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_character')
|
||||||
@patch('allianceauth.eveonline.tasks.update_alliance')
|
@patch('allianceauth.eveonline.tasks.update_alliance')
|
||||||
@patch('allianceauth.eveonline.tasks.update_corp')
|
@patch('allianceauth.eveonline.tasks.update_corp')
|
||||||
def test_run_model_update(
|
@patch('allianceauth.eveonline.providers.provider')
|
||||||
self,
|
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
|
||||||
mock_update_corp,
|
class TestRunModelUpdate(TestCase):
|
||||||
mock_update_alliance,
|
|
||||||
mock_update_character,
|
@classmethod
|
||||||
):
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
EveCorporationInfo.objects.all().delete()
|
EveCorporationInfo.objects.all().delete()
|
||||||
EveAllianceInfo.objects.all().delete()
|
EveAllianceInfo.objects.all().delete()
|
||||||
EveCharacter.objects.all().delete()
|
EveCharacter.objects.all().delete()
|
||||||
@@ -60,39 +62,184 @@ class TestTasks(TestCase):
|
|||||||
EveCorporationInfo.objects.create(
|
EveCorporationInfo.objects.create(
|
||||||
corporation_id=2345,
|
corporation_id=2345,
|
||||||
corporation_name='corp.name',
|
corporation_name='corp.name',
|
||||||
corporation_ticker='corp.ticker',
|
corporation_ticker='c.c.t',
|
||||||
member_count=10,
|
member_count=10,
|
||||||
alliance=None,
|
alliance=None,
|
||||||
)
|
)
|
||||||
EveAllianceInfo.objects.create(
|
EveAllianceInfo.objects.create(
|
||||||
alliance_id=3456,
|
alliance_id=3456,
|
||||||
alliance_name='alliance.name',
|
alliance_name='alliance.name',
|
||||||
alliance_ticker='alliance.ticker',
|
alliance_ticker='a.t',
|
||||||
executor_corp_id='78910',
|
executor_corp_id=5,
|
||||||
)
|
)
|
||||||
EveCharacter.objects.create(
|
EveCharacter.objects.create(
|
||||||
character_id=1234,
|
character_id=1,
|
||||||
character_name='character.name',
|
character_name='character.name1',
|
||||||
corporation_id=2345,
|
corporation_id=2345,
|
||||||
corporation_name='character.corp.name',
|
corporation_name='character.corp.name',
|
||||||
corporation_ticker='c.c.t', # max 5 chars
|
corporation_ticker='c.c.t', # max 5 chars
|
||||||
|
alliance_id=None
|
||||||
|
)
|
||||||
|
EveCharacter.objects.create(
|
||||||
|
character_id=2,
|
||||||
|
character_name='character.name2',
|
||||||
|
corporation_id=9876,
|
||||||
|
corporation_name='character.corp.name',
|
||||||
|
corporation_ticker='c.c.t', # max 5 chars
|
||||||
alliance_id=3456,
|
alliance_id=3456,
|
||||||
alliance_name='character.alliance.name',
|
alliance_name='character.alliance.name',
|
||||||
)
|
)
|
||||||
|
EveCharacter.objects.create(
|
||||||
|
character_id=3,
|
||||||
|
character_name='character.name3',
|
||||||
|
corporation_id=9876,
|
||||||
|
corporation_name='character.corp.name',
|
||||||
|
corporation_ticker='c.c.t', # max 5 chars
|
||||||
|
alliance_id=3456,
|
||||||
|
alliance_name='character.alliance.name',
|
||||||
|
)
|
||||||
|
EveCharacter.objects.create(
|
||||||
|
character_id=4,
|
||||||
|
character_name='character.name4',
|
||||||
|
corporation_id=9876,
|
||||||
|
corporation_name='character.corp.name',
|
||||||
|
corporation_ticker='c.c.t', # max 5 chars
|
||||||
|
alliance_id=3456,
|
||||||
|
alliance_name='character.alliance.name',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
EveCharacter.objects.create(
|
||||||
|
character_id=5,
|
||||||
|
character_name='character.name5',
|
||||||
|
corporation_id=9876,
|
||||||
|
corporation_name='character.corp.name',
|
||||||
|
corporation_ticker='c.c.t', # max 5 chars
|
||||||
|
alliance_id=3456,
|
||||||
|
alliance_name='character.alliance.name',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.affiliations = [
|
||||||
|
{'character_id': 1, 'corporation_id': 5},
|
||||||
|
{'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456},
|
||||||
|
{'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456},
|
||||||
|
{'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456}
|
||||||
|
]
|
||||||
|
self.names = [
|
||||||
|
{'id': 1, 'name': 'character.name1'},
|
||||||
|
{'id': 2, 'name': 'character.name2'},
|
||||||
|
{'id': 3, 'name': 'character.name3'},
|
||||||
|
{'id': 4, 'name': 'character.name4_new'}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_normal_run(
|
||||||
|
self,
|
||||||
|
mock_provider,
|
||||||
|
mock_update_corp,
|
||||||
|
mock_update_alliance,
|
||||||
|
mock_update_character,
|
||||||
|
):
|
||||||
|
def get_affiliations(characters: list):
|
||||||
|
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
def get_names(ids: list):
|
||||||
|
response = [x for x in self.names if x['id'] in ids]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||||
|
= get_affiliations
|
||||||
|
|
||||||
|
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||||
|
|
||||||
run_model_update()
|
run_model_update()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
mock_provider.client.Character.post_characters_affiliation.call_count, 2
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_provider.client.Universe.post_universe_names.call_count, 2
|
||||||
|
)
|
||||||
|
|
||||||
|
# character 1 has changed corp
|
||||||
|
# character 2 no change
|
||||||
|
# character 3 has changed alliance
|
||||||
|
# character 4 has changed name
|
||||||
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
|
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
|
||||||
self.assertEqual(
|
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(mock_update_alliance.apply_async.call_count, 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456
|
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456
|
||||||
)
|
)
|
||||||
|
characters_updated = {
|
||||||
|
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||||
|
}
|
||||||
|
excepted = {1, 3, 4}
|
||||||
|
self.assertSetEqual(characters_updated, excepted)
|
||||||
|
|
||||||
self.assertEqual(mock_update_character.apply_async.call_count, 1)
|
def test_ignore_character_not_in_affiliations(
|
||||||
self.assertEqual(
|
self,
|
||||||
int(mock_update_character.apply_async.call_args[1]['args'][0]), 1234
|
mock_provider,
|
||||||
)
|
mock_update_corp,
|
||||||
|
mock_update_alliance,
|
||||||
|
mock_update_character,
|
||||||
|
):
|
||||||
|
def get_affiliations(characters: list):
|
||||||
|
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
def get_names(ids: list):
|
||||||
|
response = [x for x in self.names if x['id'] in ids]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
del self.affiliations[0]
|
||||||
|
|
||||||
|
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||||
|
= get_affiliations
|
||||||
|
|
||||||
|
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||||
|
|
||||||
|
run_model_update()
|
||||||
|
characters_updated = {
|
||||||
|
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||||
|
}
|
||||||
|
excepted = {3, 4}
|
||||||
|
self.assertSetEqual(characters_updated, excepted)
|
||||||
|
|
||||||
|
def test_ignore_character_not_in_names(
|
||||||
|
self,
|
||||||
|
mock_provider,
|
||||||
|
mock_update_corp,
|
||||||
|
mock_update_alliance,
|
||||||
|
mock_update_character,
|
||||||
|
):
|
||||||
|
def get_affiliations(characters: list):
|
||||||
|
response = [x for x in self.affiliations if x['character_id'] in characters]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
def get_names(ids: list):
|
||||||
|
response = [x for x in self.names if x['id'] in ids]
|
||||||
|
mock_operator = Mock(**{'result.return_value': response})
|
||||||
|
return mock_operator
|
||||||
|
|
||||||
|
del self.names[3]
|
||||||
|
|
||||||
|
mock_provider.client.Character.post_characters_affiliation.side_effect \
|
||||||
|
= get_affiliations
|
||||||
|
|
||||||
|
mock_provider.client.Universe.post_universe_names.side_effect = get_names
|
||||||
|
|
||||||
|
run_model_update()
|
||||||
|
characters_updated = {
|
||||||
|
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
|
||||||
|
}
|
||||||
|
excepted = {1, 3}
|
||||||
|
self.assertSetEqual(characters_updated, excepted)
|
||||||
|
|||||||
37
allianceauth/groupmanagement/auth_hooks.py
Normal file
37
allianceauth/groupmanagement/auth_hooks.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
|
from allianceauth import hooks
|
||||||
|
|
||||||
|
from . import urls
|
||||||
|
from .managers import GroupManager
|
||||||
|
|
||||||
|
|
||||||
|
class GroupManagementMenuItem(MenuItemHook):
|
||||||
|
""" This class ensures only authorized users will see the menu entry """
|
||||||
|
def __init__(self):
|
||||||
|
# setup menu entry for sidebar
|
||||||
|
MenuItemHook.__init__(
|
||||||
|
self,
|
||||||
|
text=_('Group Management'),
|
||||||
|
classes='fas fa-users-cog fa-fw',
|
||||||
|
url_name='groupmanagement:management',
|
||||||
|
order=50,
|
||||||
|
navactive=['groupmanagement:management']
|
||||||
|
)
|
||||||
|
|
||||||
|
def render(self, request):
|
||||||
|
if GroupManager.can_manage_groups(request.user):
|
||||||
|
self.count = GroupManager.pending_requests_count_for_user(request.user)
|
||||||
|
return MenuItemHook.render(self, request)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('menu_item_hook')
|
||||||
|
def register_menu():
|
||||||
|
return GroupManagementMenuItem()
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('url_hook')
|
||||||
|
def register_urls():
|
||||||
|
return UrlHook(urls, 'group', r'^group/')
|
||||||
@@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, User
|
|||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
|
|
||||||
from allianceauth.authentication.models import State
|
from allianceauth.authentication.models import State
|
||||||
|
from .models import GroupRequest
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -101,3 +102,18 @@ class GroupManager:
|
|||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
|
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pending_requests_count_for_user(cls, user: User) -> int:
|
||||||
|
"""Returns the number of pending group requests for the given user"""
|
||||||
|
|
||||||
|
if cls.has_management_permission(user):
|
||||||
|
return GroupRequest.objects.filter(status="pending").count()
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
GroupRequest.objects
|
||||||
|
.filter(status="pending")
|
||||||
|
.filter(group__authgroup__group_leaders__exact=user)
|
||||||
|
.select_related("group__authgroup__group_leaders")
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.db import models
|
|||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from allianceauth.authentication.models import State
|
from allianceauth.authentication.models import State
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class GroupRequest(models.Model):
|
class GroupRequest(models.Model):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">{{ entry.date|date:"Y-M-d H:i" }}</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.requestor }}</td>
|
||||||
<td class="text-center">{{ entry.req_char }}</td>
|
<td class="text-center">{{ entry.req_char }}</td>
|
||||||
<td class="text-center">{{ entry.req_char.corporation_name }}</td>
|
<td class="text-center">{{ entry.req_char.corporation_name }}</td>
|
||||||
@@ -66,7 +66,8 @@
|
|||||||
|
|
||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
{% include 'bundles/datatables-js.html' %}
|
{% include 'bundles/datatables-js.html' %}
|
||||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
{% include 'bundles/moment-js.html' with locale=True %}
|
||||||
|
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
@@ -74,7 +75,26 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
|
|
||||||
|
$.fn.dataTable.moment = function ( format, locale ) {
|
||||||
|
var types = $.fn.dataTable.ext.type;
|
||||||
|
|
||||||
|
// Add type detection
|
||||||
|
types.detect.unshift( function ( d ) {
|
||||||
|
return moment( d, format, locale, true ).isValid() ?
|
||||||
|
'moment-'+format :
|
||||||
|
null;
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Add sorting method - use an integer for the sorting
|
||||||
|
types.order[ 'moment-'+format+'-pre' ] = function ( d ) {
|
||||||
|
return moment( d, format, locale, true ).unix();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
|
$.fn.dataTable.moment( 'YYYY-MMM-D, HH:mm' );
|
||||||
|
|
||||||
$('#log-entries').DataTable({
|
$('#log-entries').DataTable({
|
||||||
order: [[ 0, 'desc' ], [ 1, 'asc' ] ],
|
order: [[ 0, 'desc' ], [ 1, 'asc' ] ],
|
||||||
filterDropDown:
|
filterDropDown:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
|
{% include 'bundles/clipboard-js.html' %}
|
||||||
<script>
|
<script>
|
||||||
new ClipboardJS('#clipboard-copy');
|
new ClipboardJS('#clipboard-copy');
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -7,7 +7,7 @@ from django.urls import reverse
|
|||||||
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
|
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
from ..models import AuthGroup
|
from ..models import GroupRequest
|
||||||
from ..managers import GroupManager
|
from ..managers import GroupManager
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ class MockUserNotAuthenticated():
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.is_authenticated = False
|
self.is_authenticated = False
|
||||||
|
|
||||||
|
|
||||||
class GroupManagementVisibilityTestCase(TestCase):
|
class GroupManagementVisibilityTestCase(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -37,7 +38,6 @@ class GroupManagementVisibilityTestCase(TestCase):
|
|||||||
def _refresh_user(self):
|
def _refresh_user(self):
|
||||||
self.user = User.objects.get(pk=self.user.pk)
|
self.user = User.objects.get(pk=self.user.pk)
|
||||||
|
|
||||||
|
|
||||||
def test_get_group_leaders_groups(self):
|
def test_get_group_leaders_groups(self):
|
||||||
self.group1.authgroup.group_leaders.add(self.user)
|
self.group1.authgroup.group_leaders.add(self.user)
|
||||||
self.group2.authgroup.group_leader_groups.add(self.group1)
|
self.group2.authgroup.group_leader_groups.add(self.group1)
|
||||||
@@ -52,7 +52,6 @@ class GroupManagementVisibilityTestCase(TestCase):
|
|||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
groups = GroupManager.get_group_leaders_groups(self.user)
|
groups = GroupManager.get_group_leaders_groups(self.user)
|
||||||
|
|
||||||
|
|
||||||
def test_can_manage_group(self):
|
def test_can_manage_group(self):
|
||||||
self.group1.authgroup.group_leaders.add(self.user)
|
self.group1.authgroup.group_leaders.add(self.user)
|
||||||
self.user.groups.add(self.group1)
|
self.user.groups.add(self.group1)
|
||||||
@@ -182,7 +181,6 @@ class TestGroupManager(TestCase):
|
|||||||
]:
|
]:
|
||||||
self.assertFalse(GroupManager.joinable_group(x, member_state))
|
self.assertFalse(GroupManager.joinable_group(x, member_state))
|
||||||
|
|
||||||
|
|
||||||
def test_joinable_group_guest(self):
|
def test_joinable_group_guest(self):
|
||||||
guest_state = AuthUtils.get_guest_state()
|
guest_state = AuthUtils.get_guest_state()
|
||||||
for x in [
|
for x in [
|
||||||
@@ -200,7 +198,6 @@ class TestGroupManager(TestCase):
|
|||||||
]:
|
]:
|
||||||
self.assertFalse(GroupManager.joinable_group(x, guest_state))
|
self.assertFalse(GroupManager.joinable_group(x, guest_state))
|
||||||
|
|
||||||
|
|
||||||
def test_get_all_non_internal_groups(self):
|
def test_get_all_non_internal_groups(self):
|
||||||
result = GroupManager.get_all_non_internal_groups()
|
result = GroupManager.get_all_non_internal_groups()
|
||||||
expected = {
|
expected = {
|
||||||
@@ -335,3 +332,96 @@ class TestGroupManager(TestCase):
|
|||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
GroupManager.can_manage_group(user, self.group_default)
|
GroupManager.can_manage_group(user, self.group_default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPendingRequestsCountForUser(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.group_1 = Group.objects.create(name="Group 1")
|
||||||
|
self.group_2 = Group.objects.create(name="Group 2")
|
||||||
|
self.user_leader_1 = AuthUtils.create_member('Clark Kent')
|
||||||
|
self.group_1.authgroup.group_leaders.add(self.user_leader_1)
|
||||||
|
self.user_leader_2 = AuthUtils.create_member('Peter Parker')
|
||||||
|
self.group_2.authgroup.group_leaders.add(self.user_leader_2)
|
||||||
|
self.user_requestor = AuthUtils.create_member('Bruce Wayne')
|
||||||
|
|
||||||
|
def test_single_request_for_leader(self):
|
||||||
|
# given user_leader_1 is leader of group_1
|
||||||
|
# and user_leader_2 is leader of group_2
|
||||||
|
# when user_requestor is requesting access to group 1
|
||||||
|
# then return 1 for user_leader 1 and 0 for user_leader_2
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending", user=self.user_requestor, group=self.group_1
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_leader_2), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_return_none_for_none_leader(self):
|
||||||
|
# given user_requestor is leader of no group
|
||||||
|
# when user_requestor is requesting access to group 1
|
||||||
|
# then return 0 for user_requestor
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending", user=self.user_requestor, group=self.group_1
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_requestor), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_leave_request(self):
|
||||||
|
# given user_leader_2 is leader of group_2
|
||||||
|
# and user_requestor is member of group 2
|
||||||
|
# when user_requestor is requesting to leave group 2
|
||||||
|
# then return 1 for user_leader_2
|
||||||
|
self.user_requestor.groups.add(self.group_2)
|
||||||
|
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending",
|
||||||
|
user=self.user_requestor,
|
||||||
|
group=self.group_2,
|
||||||
|
leave_request=True
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_leader_2), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_join_and_leave_request(self):
|
||||||
|
# given user_leader_2 is leader of group_2
|
||||||
|
# and user_requestor is member of group 2
|
||||||
|
# when user_requestor is requesting to leave group 2
|
||||||
|
# and user_requestor_2 is requesting to join group 2
|
||||||
|
# then return 2 for user_leader_2
|
||||||
|
self.user_requestor.groups.add(self.group_2)
|
||||||
|
user_requestor_2 = AuthUtils.create_member("Lex Luther")
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending",
|
||||||
|
user=user_requestor_2,
|
||||||
|
group=self.group_2
|
||||||
|
)
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending",
|
||||||
|
user=self.user_requestor,
|
||||||
|
group=self.group_2,
|
||||||
|
leave_request=True
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_leader_2), 2
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_request_for_user_with_management_perm(self):
|
||||||
|
# given user_leader_4 which is leafer of no group
|
||||||
|
# but has the management permissions
|
||||||
|
# when user_requestor is requesting access to group 1
|
||||||
|
# then return 1 for user_leader_4
|
||||||
|
user_leader_4 = AuthUtils.create_member("Lex Luther")
|
||||||
|
AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4)
|
||||||
|
user_leader_4 = User.objects.get(pk=user_leader_4.pk)
|
||||||
|
GroupRequest.objects.create(
|
||||||
|
status="pending", user=self.user_requestor, group=self.group_1
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import url
|
||||||
app_name = 'groupmanagement'
|
app_name = 'groupmanagement'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^groups/', views.groups_view, name='groups'),
|
url(r'^groups/', views.groups_view, name='groups'),
|
||||||
url(r'^group/', include([
|
|
||||||
url(r'^management/', views.group_management,
|
url(r'^management/', views.group_management,
|
||||||
name='management'),
|
name='management'),
|
||||||
url(r'^membership/$', views.group_membership,
|
url(r'^membership/$', views.group_membership,
|
||||||
@@ -21,12 +20,10 @@ urlpatterns = [
|
|||||||
name='accept_request'),
|
name='accept_request'),
|
||||||
url(r'^request/reject/(\w+)', views.group_reject_request,
|
url(r'^request/reject/(\w+)', views.group_reject_request,
|
||||||
name='reject_request'),
|
name='reject_request'),
|
||||||
|
|
||||||
url(r'^request_leave/(\w+)', views.group_request_leave,
|
url(r'^request_leave/(\w+)', views.group_request_leave,
|
||||||
name='request_leave'),
|
name='request_leave'),
|
||||||
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
|
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
|
||||||
name='leave_accept_request'),
|
name='leave_accept_request'),
|
||||||
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
|
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
|
||||||
name='leave_reject_request'),
|
name='leave_reject_request'),
|
||||||
])),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
from allianceauth.hrapplications import urls
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
|
|
||||||
|
from . import urls
|
||||||
|
from .models import Application
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsMenu(MenuItemHook):
|
class ApplicationsMenu(MenuItemHook):
|
||||||
@@ -12,6 +15,11 @@ class ApplicationsMenu(MenuItemHook):
|
|||||||
'hrapplications:index',
|
'hrapplications:index',
|
||||||
navactive=['hrapplications:'])
|
navactive=['hrapplications:'])
|
||||||
|
|
||||||
|
def render(self, request):
|
||||||
|
app_count = Application.objects.pending_requests_count_for_user(request.user)
|
||||||
|
self.count = app_count if app_count and app_count > 0 else None
|
||||||
|
return MenuItemHook.render(self, request)
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('menu_item_hook')
|
@hooks.register('menu_item_hook')
|
||||||
def register_menu():
|
def register_menu():
|
||||||
|
|||||||
25
allianceauth/hrapplications/managers.py
Normal file
25
allianceauth/hrapplications/managers.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationManager(models.Manager):
|
||||||
|
|
||||||
|
def pending_requests_count_for_user(self, user: User) -> Optional[int]:
|
||||||
|
"""Returns the number of pending group requests for the given user"""
|
||||||
|
if user.is_superuser:
|
||||||
|
return self.filter(approved__isnull=True).count()
|
||||||
|
elif user.has_perm("auth.human_resources"):
|
||||||
|
main_character = user.profile.main_character
|
||||||
|
if main_character:
|
||||||
|
return (
|
||||||
|
self
|
||||||
|
.select_related("form__corp")
|
||||||
|
.filter(form__corp__corporation_id=main_character.corporation_id)
|
||||||
|
.filter(approved__isnull=True)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -2,8 +2,9 @@ from django.contrib.auth.models import User
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
|
|
||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
|
||||||
from allianceauth.eveonline.models import EveCorporationInfo
|
|
||||||
|
from .managers import ApplicationManager
|
||||||
|
|
||||||
|
|
||||||
class ApplicationQuestion(models.Model):
|
class ApplicationQuestion(models.Model):
|
||||||
@@ -22,6 +23,7 @@ class ApplicationChoice(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.choice_text
|
return self.choice_text
|
||||||
|
|
||||||
|
|
||||||
class ApplicationForm(models.Model):
|
class ApplicationForm(models.Model):
|
||||||
questions = SortedManyToManyField(ApplicationQuestion)
|
questions = SortedManyToManyField(ApplicationQuestion)
|
||||||
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
||||||
@@ -38,6 +40,8 @@ class Application(models.Model):
|
|||||||
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
objects = ApplicationManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user) + " Application To " + str(self.form)
|
return str(self.user) + " Application To " + str(self.form)
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,103 @@
|
|||||||
# Create your tests here.
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.eveonline.models import EveCorporationInfo
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from .models import Application, ApplicationForm, ApplicationQuestion, ApplicationChoice
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplicationManagersPendingRequestsCountForUser(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.corporation_1 = EveCorporationInfo.objects.create(
|
||||||
|
corporation_id=2001, corporation_name="Wayne Tech", member_count=42
|
||||||
|
)
|
||||||
|
self.corporation_2 = EveCorporationInfo.objects.create(
|
||||||
|
corporation_id=2011, corporation_name="Lex Corp", member_count=666
|
||||||
|
)
|
||||||
|
question = ApplicationQuestion.objects.create(title="Dummy Question")
|
||||||
|
ApplicationChoice.objects.create(question=question, choice_text="yes")
|
||||||
|
ApplicationChoice.objects.create(question=question, choice_text="no")
|
||||||
|
self.form_corporation_1 = ApplicationForm.objects.create(
|
||||||
|
corp=self.corporation_1
|
||||||
|
)
|
||||||
|
self.form_corporation_1.questions.add(question)
|
||||||
|
self.form_corporation_2 = ApplicationForm.objects.create(
|
||||||
|
corp=self.corporation_2
|
||||||
|
)
|
||||||
|
self.form_corporation_2.questions.add(question)
|
||||||
|
|
||||||
|
self.user_requestor = AuthUtils.create_member("Peter Parker")
|
||||||
|
|
||||||
|
self.user_manager = AuthUtils.create_member("Bruce Wayne")
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
self.user_manager,
|
||||||
|
self.user_manager.username,
|
||||||
|
1001,
|
||||||
|
self.corporation_1.corporation_id,
|
||||||
|
self.corporation_1.corporation_name,
|
||||||
|
)
|
||||||
|
AuthUtils.add_permission_to_user_by_name(
|
||||||
|
"auth.human_resources", self.user_manager
|
||||||
|
)
|
||||||
|
self.user_manager = User.objects.get(pk=self.user_manager.pk)
|
||||||
|
|
||||||
|
def test_no_pending_application(self):
|
||||||
|
# given manager of corporation 1 has permission
|
||||||
|
# when no application is pending for corporation 1
|
||||||
|
# return 0
|
||||||
|
self.assertEqual(
|
||||||
|
Application.objects.pending_requests_count_for_user(self.user_manager), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_pending_application(self):
|
||||||
|
# given manager of corporation 1 has permission
|
||||||
|
# when 1 application is pending for corporation 1
|
||||||
|
# return 1
|
||||||
|
Application.objects.create(
|
||||||
|
form=self.form_corporation_1, user=self.user_requestor
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Application.objects.pending_requests_count_for_user(self.user_manager), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_user_has_no_permission(self):
|
||||||
|
# given user has no permission
|
||||||
|
# when 1 application is pending
|
||||||
|
# return None
|
||||||
|
self.assertIsNone(
|
||||||
|
Application.objects.pending_requests_count_for_user(self.user_requestor)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_two_pending_applications_for_different_corporations_normal_manager(self):
|
||||||
|
# given manager of corporation 1 has permission
|
||||||
|
# when 1 application is pending for corporation 1
|
||||||
|
# and 1 application is pending for corporation 2
|
||||||
|
# return 1
|
||||||
|
Application.objects.create(
|
||||||
|
form=self.form_corporation_1, user=self.user_requestor
|
||||||
|
)
|
||||||
|
Application.objects.create(
|
||||||
|
form=self.form_corporation_2, user=self.user_requestor
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Application.objects.pending_requests_count_for_user(self.user_manager), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_two_pending_applications_for_different_corporations_manager_is_super(self):
|
||||||
|
# given manager of corporation 1 has permission
|
||||||
|
# when 1 application is pending for corporation 1
|
||||||
|
# and 1 application is pending for corporation 2
|
||||||
|
# return 1
|
||||||
|
Application.objects.create(
|
||||||
|
form=self.form_corporation_1, user=self.user_requestor
|
||||||
|
)
|
||||||
|
Application.objects.create(
|
||||||
|
form=self.form_corporation_2, user=self.user_requestor
|
||||||
|
)
|
||||||
|
superuser = User.objects.create_superuser(
|
||||||
|
"Superman", "superman@example.com", "password"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
Application.objects.pending_requests_count_for_user(superuser), 2
|
||||||
|
)
|
||||||
|
|||||||
@@ -36,9 +36,15 @@
|
|||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
|
|
||||||
$('#id_start').datetimepicker({
|
$('#id_start').datetimepicker({
|
||||||
lang: '{{ LANGUAGE_CODE }}',
|
setlocale: '{{ LANGUAGE_CODE }}',
|
||||||
maskInput: true,
|
{% if NIGHT_MODE %}
|
||||||
format: 'Y-m-d H:i',minDate:0
|
theme: 'dark',
|
||||||
|
{% else %}
|
||||||
|
theme: 'default',
|
||||||
|
{% endif %}
|
||||||
|
mask: true,
|
||||||
|
format: 'Y-m-d H:i',
|
||||||
|
minDate: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endblock extra_script %}
|
{% endblock extra_script %}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
{% include 'bundles/moment-js.html' with locale=True %}
|
{% include 'bundles/moment-js.html' with locale=True %}
|
||||||
<script src="{% static 'js/timers.js' %}"></script>
|
<script src="{% static 'js/timers.js' %}"></script>
|
||||||
<script type="text/javascript">
|
<script type="application/javascript">
|
||||||
// Data
|
// Data
|
||||||
var timers = [
|
var timers = [
|
||||||
{% for op in optimer %}
|
{% for op in optimer %}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="application/javascript">
|
||||||
|
|
||||||
timedUpdate();
|
timedUpdate();
|
||||||
setAllLocalTimes();
|
setAllLocalTimes();
|
||||||
|
|||||||
@@ -44,9 +44,15 @@
|
|||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
|
|
||||||
$('#id_start').datetimepicker({
|
$('#id_start').datetimepicker({
|
||||||
lang: '{{ LANGUAGE_CODE }}',
|
setlocale: '{{ LANGUAGE_CODE }}',
|
||||||
maskInput: true,
|
{% if NIGHT_MODE %}
|
||||||
format: 'Y-m-d H:i',minDate:0
|
theme: 'dark',
|
||||||
|
{% else %}
|
||||||
|
theme: 'default',
|
||||||
|
{% endif %}
|
||||||
|
mask: true,
|
||||||
|
format: 'Y-m-d H:i',
|
||||||
|
minDate: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endblock extra_script %}
|
{% endblock extra_script %}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
{% include 'bundles/datatables-js.html' %}
|
{% include 'bundles/datatables-js.html' %}
|
||||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
{% include 'bundles/datatables-js.html' %}
|
{% include 'bundles/datatables-js.html' %}
|
||||||
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ class MenuItemHook:
|
|||||||
self.url_name = url_name
|
self.url_name = url_name
|
||||||
self.template = 'public/menuitem.html'
|
self.template = 'public/menuitem.html'
|
||||||
self.order = order if order is not None else 9999
|
self.order = order if order is not None else 9999
|
||||||
|
|
||||||
|
# count is an integer shown next to the menu item as badge when count != None
|
||||||
|
# apps need to set the count in their child class, e.g. in render() method
|
||||||
|
self.count = None
|
||||||
|
|
||||||
navactive = navactive or []
|
navactive = navactive or []
|
||||||
navactive.append(url_name)
|
navactive.append(url_name)
|
||||||
self.navactive = navactive
|
self.navactive = navactive
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from ..utils import clean_setting
|
|||||||
|
|
||||||
# Base URL for all API calls. Must end with /.
|
# Base URL for all API calls. Must end with /.
|
||||||
DISCORD_API_BASE_URL = clean_setting(
|
DISCORD_API_BASE_URL = clean_setting(
|
||||||
'DISCORD_API_BASE_URL', 'https://discordapp.com/api/'
|
'DISCORD_API_BASE_URL', 'https://discord.com/api/'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Low level connecttimeout for requests to the Discord API in seconds
|
# Low level connecttimeout for requests to the Discord API in seconds
|
||||||
@@ -18,12 +18,12 @@ DISCORD_API_TIMEOUT_READ = clean_setting(
|
|||||||
|
|
||||||
# Base authorization URL for Discord Oauth
|
# Base authorization URL for Discord Oauth
|
||||||
DISCORD_OAUTH_BASE_URL = clean_setting(
|
DISCORD_OAUTH_BASE_URL = clean_setting(
|
||||||
'DISCORD_OAUTH_BASE_URL', 'https://discordapp.com/api/oauth2/authorize'
|
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Base authorization URL for Discord Oauth
|
# Base authorization URL for Discord Oauth
|
||||||
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
||||||
'DISCORD_OAUTH_TOKEN_URL', 'https://discordapp.com/api/oauth2/token'
|
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
|
||||||
)
|
)
|
||||||
|
|
||||||
# How long the Discord guild names retrieved from the server are
|
# How long the Discord guild names retrieved from the server are
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ logger = set_logger_to_file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
|
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
|
||||||
API_BASE_URL = 'https://discordapp.com/api/'
|
API_BASE_URL = 'https://discord.com/api/'
|
||||||
|
|
||||||
TEST_RETRY_AFTER = 3000
|
TEST_RETRY_AFTER = 3000
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from . import providers
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
|
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
|
||||||
@@ -19,128 +19,8 @@ class DiscourseError(Exception):
|
|||||||
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
||||||
|
|
||||||
|
|
||||||
# not exhaustive, only the ones we need
|
|
||||||
ENDPOINTS = {
|
|
||||||
'groups': {
|
|
||||||
'list': {
|
|
||||||
'path': "/groups/search.json",
|
|
||||||
'method': 'get',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'create': {
|
|
||||||
'path': "/admin/groups",
|
|
||||||
'method': 'post',
|
|
||||||
'args': {
|
|
||||||
'required': ['name'],
|
|
||||||
'optional': ['visible'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'add_user': {
|
|
||||||
'path': "/admin/groups/%s/members.json",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': ['usernames'],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'remove_user': {
|
|
||||||
'path': "/admin/groups/%s/members.json",
|
|
||||||
'method': 'delete',
|
|
||||||
'args': {
|
|
||||||
'required': ['username'],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'delete': {
|
|
||||||
'path': "/admin/groups/%s.json",
|
|
||||||
'method': 'delete',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'users': {
|
|
||||||
'create': {
|
|
||||||
'path': "/users",
|
|
||||||
'method': 'post',
|
|
||||||
'args': {
|
|
||||||
'required': ['name', 'email', 'password', 'username'],
|
|
||||||
'optional': ['active'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'update': {
|
|
||||||
'path': "/users/%s.json",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': ['params'],
|
|
||||||
'optional': [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'get': {
|
|
||||||
'path': "/users/%s.json",
|
|
||||||
'method': 'get',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'activate': {
|
|
||||||
'path': "/admin/users/%s/activate",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'set_email': {
|
|
||||||
'path': "/users/%s/preferences/email",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': ['email'],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'suspend': {
|
|
||||||
'path': "/admin/users/%s/suspend",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': ['duration', 'reason'],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'unsuspend': {
|
|
||||||
'path': "/admin/users/%s/unsuspend",
|
|
||||||
'method': 'put',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'logout': {
|
|
||||||
'path': "/admin/users/%s/log_out",
|
|
||||||
'method': 'post',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'external': {
|
|
||||||
'path': "/users/by-external/%s.json",
|
|
||||||
'method': 'get',
|
|
||||||
'args': {
|
|
||||||
'required': [],
|
|
||||||
'optional': [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DiscourseManager:
|
class DiscourseManager:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -148,55 +28,14 @@ class DiscourseManager:
|
|||||||
SUSPEND_DAYS = 99999
|
SUSPEND_DAYS = 99999
|
||||||
SUSPEND_REASON = "Disabled by auth."
|
SUSPEND_REASON = "Disabled by auth."
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __exc(endpoint, *args, **kwargs):
|
|
||||||
params = {
|
|
||||||
'api_key': settings.DISCOURSE_API_KEY,
|
|
||||||
'api_username': settings.DISCOURSE_API_USERNAME,
|
|
||||||
}
|
|
||||||
silent = kwargs.pop('silent', False)
|
|
||||||
if args:
|
|
||||||
endpoint['parsed_url'] = endpoint['path'] % args
|
|
||||||
else:
|
|
||||||
endpoint['parsed_url'] = endpoint['path']
|
|
||||||
data = {}
|
|
||||||
for arg in endpoint['args']['required']:
|
|
||||||
data[arg] = kwargs[arg]
|
|
||||||
for arg in endpoint['args']['optional']:
|
|
||||||
if arg in kwargs:
|
|
||||||
data[arg] = kwargs[arg]
|
|
||||||
for arg in kwargs:
|
|
||||||
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
|
|
||||||
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
|
||||||
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], headers=params,
|
|
||||||
json=data)
|
|
||||||
try:
|
|
||||||
if 'errors' in r.json() and not silent:
|
|
||||||
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
|
||||||
raise DiscourseError(endpoint, r.json()['errors'])
|
|
||||||
if 'success' in r.json():
|
|
||||||
if not r.json()['success'] and not silent:
|
|
||||||
raise DiscourseError(endpoint, None)
|
|
||||||
out = r.json()
|
|
||||||
except ValueError:
|
|
||||||
out = r.text
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
r.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as e:
|
|
||||||
raise DiscourseError(endpoint, e.response.status_code)
|
|
||||||
return out
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_groups():
|
def _get_groups():
|
||||||
endpoint = ENDPOINTS['groups']['list']
|
data = providers.discourse.client.groups()
|
||||||
data = DiscourseManager.__exc(endpoint)
|
|
||||||
return [g for g in data if not g['automatic']]
|
return [g for g in data if not g['automatic']]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_group(name):
|
def _create_group(name):
|
||||||
endpoint = ENDPOINTS['groups']['create']
|
return providers.discourse.client.create_group(name=name[:20], visible=True)['basic_group']
|
||||||
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_cache_group_name_key(name):
|
def _generate_cache_group_name_key(name):
|
||||||
@@ -234,13 +73,11 @@ class DiscourseManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __add_user_to_group(g_id, username):
|
def __add_user_to_group(g_id, username):
|
||||||
endpoint = ENDPOINTS['groups']['add_user']
|
providers.discourse.client.add_group_member(g_id, username)
|
||||||
DiscourseManager.__exc(endpoint, g_id, usernames=username)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __remove_user_from_group(g_id, username):
|
def __remove_user_from_group(g_id, uid):
|
||||||
endpoint = ENDPOINTS['groups']['remove_user']
|
providers.discourse.client.delete_group_member(g_id, uid)
|
||||||
DiscourseManager.__exc(endpoint, g_id, username=username)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __generate_group_dict(names):
|
def __generate_group_dict(names):
|
||||||
@@ -252,39 +89,35 @@ class DiscourseManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user_groups(username):
|
def __get_user_groups(username):
|
||||||
data = DiscourseManager.__get_user(username)
|
data = DiscourseManager.__get_user(username)
|
||||||
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
|
return [g['id'] for g in data['groups'] if not g['automatic']]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __user_name_to_id(name, silent=False):
|
def __user_name_to_id(name, silent=False):
|
||||||
data = DiscourseManager.__get_user(name, silent=silent)
|
data = DiscourseManager.__get_user(name)
|
||||||
return data['user']['id']
|
return data['user']['id']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user(username, silent=False):
|
def __get_user(username, silent=False):
|
||||||
endpoint = ENDPOINTS['users']['get']
|
return providers.discourse.client.user(username)
|
||||||
return DiscourseManager.__exc(endpoint, username, silent=silent)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __activate_user(username):
|
def __activate_user(username):
|
||||||
endpoint = ENDPOINTS['users']['activate']
|
|
||||||
u_id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
DiscourseManager.__exc(endpoint, u_id)
|
providers.discourse.client.activate(u_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __update_user(username, **kwargs):
|
def __update_user(username, **kwargs):
|
||||||
endpoint = ENDPOINTS['users']['update']
|
|
||||||
u_id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
|
providers.discourse.client.update_user(endpoint, u_id, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __create_user(username, email, password):
|
def __create_user(username, email, password):
|
||||||
endpoint = ENDPOINTS['users']['create']
|
providers.discourse.client.create_user(username, username, email, password)
|
||||||
DiscourseManager.__exc(endpoint, name=username, username=username, email=email, password=password, active=True)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __check_if_user_exists(username):
|
def __check_if_user_exists(username):
|
||||||
try:
|
try:
|
||||||
DiscourseManager.__user_name_to_id(username, silent=True)
|
DiscourseManager.__user_name_to_id(username)
|
||||||
return True
|
return True
|
||||||
except DiscourseError:
|
except DiscourseError:
|
||||||
return False
|
return False
|
||||||
@@ -292,30 +125,26 @@ class DiscourseManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __suspend_user(username):
|
def __suspend_user(username):
|
||||||
u_id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
endpoint = ENDPOINTS['users']['suspend']
|
return providers.discourse.client.suspend(u_id, DiscourseManager.SUSPEND_DAYS,
|
||||||
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
|
DiscourseManager.SUSPEND_REASON)
|
||||||
reason=DiscourseManager.SUSPEND_REASON)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __unsuspend(username):
|
def __unsuspend(username):
|
||||||
u_id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
endpoint = ENDPOINTS['users']['unsuspend']
|
return providers.discourse.client.unsuspend(u_id)
|
||||||
return DiscourseManager.__exc(endpoint, u_id)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_email(username, email):
|
def __set_email(username, email):
|
||||||
endpoint = ENDPOINTS['users']['set_email']
|
return providers.discourse.client.update_email(username, email)
|
||||||
return DiscourseManager.__exc(endpoint, username, email=email)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __logout(u_id):
|
def __logout(u_id):
|
||||||
endpoint = ENDPOINTS['users']['logout']
|
return providers.discourse.client.log_out(u_id)
|
||||||
return DiscourseManager.__exc(endpoint, u_id)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user_by_external(u_id):
|
def __get_user_by_external(u_id):
|
||||||
endpoint = ENDPOINTS['users']['external']
|
data = providers.discourse.client.user_by_external_id(u_id)
|
||||||
return DiscourseManager.__exc(endpoint, u_id)
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __user_id_by_external_id(u_id):
|
def __user_id_by_external_id(u_id):
|
||||||
@@ -351,7 +180,9 @@ class DiscourseManager:
|
|||||||
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
||||||
group_dict = DiscourseManager.__generate_group_dict(groups)
|
group_dict = DiscourseManager.__generate_group_dict(groups)
|
||||||
inv_group_dict = {v: k for k, v in group_dict.items()}
|
inv_group_dict = {v: k for k, v in group_dict.items()}
|
||||||
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
|
discord_user = DiscourseManager.__get_user_by_external(user.pk)
|
||||||
|
username = discord_user['username']
|
||||||
|
uid = discord_user['id']
|
||||||
user_groups = DiscourseManager.__get_user_groups(username)
|
user_groups = DiscourseManager.__get_user_groups(username)
|
||||||
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
||||||
rem_groups = [x for x in user_groups if x not in inv_group_dict]
|
rem_groups = [x for x in user_groups if x not in inv_group_dict]
|
||||||
@@ -364,7 +195,7 @@ class DiscourseManager:
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Updating discourse user %s groups: removing %s" % (username, rem_groups))
|
"Updating discourse user %s groups: removing %s" % (username, rem_groups))
|
||||||
for g in rem_groups:
|
for g in rem_groups:
|
||||||
DiscourseManager.__remove_user_from_group(g, username)
|
DiscourseManager.__remove_user_from_group(g, uid)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def disable_user(user):
|
def disable_user(user):
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ class DiscourseUser(models.Model):
|
|||||||
permissions = (
|
permissions = (
|
||||||
("access_discourse", u"Can access the Discourse service"),
|
("access_discourse", u"Can access the Discourse service"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
19
allianceauth/services/modules/discourse/providers.py
Normal file
19
allianceauth/services/modules/discourse/providers.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from pydiscourse import DiscourseClient
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class DiscourseAPIClient():
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if not self._client:
|
||||||
|
self._client = DiscourseClient(
|
||||||
|
settings.DISCOURSE_URL,
|
||||||
|
api_username=settings.DISCOURSE_API_USERNAME,
|
||||||
|
api_key=settings.DISCOURSE_API_KEY)
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
discourse = DiscourseAPIClient()
|
||||||
@@ -47,7 +47,8 @@ class DiscourseTasks:
|
|||||||
logger.debug("Updating discourse groups for user %s" % user)
|
logger.debug("Updating discourse groups for user %s" % user)
|
||||||
try:
|
try:
|
||||||
DiscourseManager.update_groups(user)
|
DiscourseManager.update_groups(user)
|
||||||
except:
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||||
raise self.retry(countdown=60 * 10)
|
raise self.retry(countdown=60 * 10)
|
||||||
logger.debug("Updated user %s discourse groups." % user)
|
logger.debug("Updated user %s discourse groups." % user)
|
||||||
@@ -63,3 +64,4 @@ class DiscourseTasks:
|
|||||||
def get_username(user):
|
def get_username(user):
|
||||||
from .auth_hooks import DiscourseService
|
from .auth_hooks import DiscourseService
|
||||||
return NameFormatter(DiscourseService(), user).format_name()
|
return NameFormatter(DiscourseService(), user).format_name()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.contrib.auth.models import User
|
|||||||
from .hooks import ServicesHook
|
from .hooks import ServicesHook
|
||||||
from celery_once import QueueOnce as BaseTask, AlreadyQueued
|
from celery_once import QueueOnce as BaseTask, AlreadyQueued
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from time import time
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -22,14 +21,9 @@ class DjangoBackend:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def raise_or_lock(key, timeout):
|
def raise_or_lock(key, timeout):
|
||||||
now = int(time())
|
acquired = cache.add(key=key, value="lock", timeout=timeout)
|
||||||
result = cache.get(key)
|
if not acquired:
|
||||||
if result:
|
raise AlreadyQueued(int(cache.ttl(key)))
|
||||||
remaining = int(result) - now
|
|
||||||
if remaining > 0:
|
|
||||||
raise AlreadyQueued(remaining)
|
|
||||||
else:
|
|
||||||
cache.set(key, now + timeout, timeout)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clear_lock(key):
|
def clear_lock(key):
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
{% load navactive %}
|
{% load navactive %}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a class="{% navactive request item.navactive|join:" " %}" href="{% url item.url_name %}">
|
<a class="{% navactive request item.navactive|join:' ' %}" href="{% url item.url_name %}">
|
||||||
<i class="{{ item.classes }}"></i> {% trans item.text %}
|
<i class="{{ item.classes }}"></i> {% trans item.text %}
|
||||||
|
{% if item.count != None %}
|
||||||
|
<span class="badge">{{ item.count }}</span>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from celery_once import AlreadyQueued
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
from allianceauth.services.tasks import validate_services
|
from allianceauth.services.tasks import validate_services
|
||||||
|
|
||||||
|
from ..tasks import DjangoBackend
|
||||||
|
|
||||||
|
|
||||||
class ServicesTasksTestCase(TestCase):
|
class ServicesTasksTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -24,3 +28,46 @@ class ServicesTasksTestCase(TestCase):
|
|||||||
self.assertTrue(svc.validate_user.called)
|
self.assertTrue(svc.validate_user.called)
|
||||||
args, kwargs = svc.validate_user.call_args
|
args, kwargs = svc.validate_user.call_args
|
||||||
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
|
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
|
||||||
|
|
||||||
|
|
||||||
|
class TestDjangoBackend(TestCase):
|
||||||
|
|
||||||
|
TEST_KEY = "my-django-backend-test-key"
|
||||||
|
TIMEOUT = 1800
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
cache.delete(self.TEST_KEY)
|
||||||
|
self.backend = DjangoBackend(dict())
|
||||||
|
|
||||||
|
def test_can_get_lock(self):
|
||||||
|
"""
|
||||||
|
when lock can be acquired
|
||||||
|
then set it with timetout
|
||||||
|
"""
|
||||||
|
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||||
|
self.assertIsNotNone(cache.get(self.TEST_KEY))
|
||||||
|
self.assertAlmostEqual(cache.ttl(self.TEST_KEY), self.TIMEOUT, delta=2)
|
||||||
|
|
||||||
|
def test_when_cant_get_lock_raise_exception(self):
|
||||||
|
"""
|
||||||
|
when lock can bot be acquired
|
||||||
|
then raise AlreadyQueued exception with countdown
|
||||||
|
"""
|
||||||
|
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||||
|
except Exception as ex:
|
||||||
|
self.assertIsInstance(ex, AlreadyQueued)
|
||||||
|
self.assertAlmostEqual(ex.countdown, self.TIMEOUT, delta=2)
|
||||||
|
|
||||||
|
def test_can_clear_lock(self):
|
||||||
|
"""
|
||||||
|
when a lock exists
|
||||||
|
then can get a new lock after clearing it
|
||||||
|
"""
|
||||||
|
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||||
|
|
||||||
|
self.backend.clear_lock(self.TEST_KEY)
|
||||||
|
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
|
||||||
|
self.assertIsNotNone(cache.get(self.TEST_KEY))
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from allianceauth import hooks
|
from allianceauth import hooks
|
||||||
|
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||||
|
|
||||||
from . import urls
|
from . import urls
|
||||||
|
from .managers import SRPManager
|
||||||
|
|
||||||
|
|
||||||
class SrpMenu(MenuItemHook):
|
class SrpMenu(MenuItemHook):
|
||||||
@@ -13,6 +16,8 @@ class SrpMenu(MenuItemHook):
|
|||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
if request.user.has_perm('srp.access_srp'):
|
if request.user.has_perm('srp.access_srp'):
|
||||||
|
app_count = SRPManager.pending_requests_count_for_user(request.user)
|
||||||
|
self.count = app_count if app_count and app_count > 0 else None
|
||||||
return MenuItemHook.render(self, request)
|
return MenuItemHook.render(self, request)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from allianceauth import NAME
|
from allianceauth import NAME
|
||||||
from allianceauth.eveonline.providers import provider
|
from allianceauth.eveonline.providers import provider
|
||||||
|
|
||||||
|
from .models import SrpUserRequest
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -50,3 +52,12 @@ class SRPManager:
|
|||||||
return ship_type, ship_value, victim_id
|
return ship_type, ship_value, victim_id
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid Kill ID or Hash.")
|
raise ValueError("Invalid Kill ID or Hash.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pending_requests_count_for_user(user: User):
|
||||||
|
"""returns the number of open SRP requests for given user
|
||||||
|
or None if user has no permission"""
|
||||||
|
if user.has_perm("auth.srp_management"):
|
||||||
|
return SrpUserRequest.objects.filter(srp_status="pending").count()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info" role="alert">{% blocktrans %}Give this link to the line members{% endblocktrans %}.</div>
|
<div class="alert alert-info" role="alert">{% blocktrans %}Give this link to the line members{% endblocktrans %}.</div>
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
http://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
|
{{ request.scheme }}://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="{% url 'srp:management' %}" class="btn btn-primary btn-lg">{% trans "Continue" %}</a>
|
<a href="{% url 'srp:management' %}" class="btn btn-primary btn-lg">{% trans "Continue" %}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -46,8 +45,15 @@
|
|||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
|
|
||||||
$('#id_fleet_time').datetimepicker({
|
$('#id_fleet_time').datetimepicker({
|
||||||
maskInput: true,
|
setlocale: '{{ LANGUAGE_CODE }}',
|
||||||
format: 'Y-m-d H:i'
|
{% if NIGHT_MODE %}
|
||||||
|
theme: 'dark',
|
||||||
|
{% else %}
|
||||||
|
theme: 'default',
|
||||||
|
{% endif %}
|
||||||
|
mask: true,
|
||||||
|
format: 'Y-m-d H:i',
|
||||||
|
minDate: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
{% endblock extra_script %}
|
{% endblock extra_script %}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch, Mock
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
from ..managers import SRPManager
|
from ..managers import SRPManager
|
||||||
|
from ..models import SrpUserRequest, SrpFleetMain
|
||||||
|
|
||||||
MODULE_PATH = 'allianceauth.srp.managers'
|
MODULE_PATH = 'allianceauth.srp.managers'
|
||||||
|
|
||||||
@@ -13,6 +18,7 @@ currentdir = os.path.dirname(os.path.abspath(inspect.getfile(
|
|||||||
inspect.currentframe()
|
inspect.currentframe()
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
def load_data(filename):
|
def load_data(filename):
|
||||||
"""loads given JSON file from `testdata` sub folder and returns content"""
|
"""loads given JSON file from `testdata` sub folder and returns content"""
|
||||||
with open(
|
with open(
|
||||||
@@ -52,7 +58,7 @@ class TestSrpManager(TestCase):
|
|||||||
mock_get.return_value.json.return_value = ['']
|
mock_get.return_value.json.return_value = ['']
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
|
SRPManager.get_kill_data(81973979)
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.provider')
|
@patch(MODULE_PATH + '.provider')
|
||||||
@patch(MODULE_PATH + '.requests.get')
|
@patch(MODULE_PATH + '.requests.get')
|
||||||
@@ -67,6 +73,34 @@ class TestSrpManager(TestCase):
|
|||||||
result.return_value = None
|
result.return_value = None
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
|
SRPManager.get_kill_data(81973979)
|
||||||
|
|
||||||
|
def test_pending_requests_count_for_user(self):
|
||||||
|
user = AuthUtils.create_member("Bruce Wayne")
|
||||||
|
|
||||||
|
# when no permission to approve SRP requests
|
||||||
|
# then return None
|
||||||
|
self.assertIsNone(SRPManager.pending_requests_count_for_user(user))
|
||||||
|
|
||||||
|
# given permission to approve SRP requests
|
||||||
|
# when no open requests
|
||||||
|
# then return 0
|
||||||
|
AuthUtils.add_permission_to_user_by_name("auth.srp_management", user)
|
||||||
|
user = User.objects.get(pk=user.pk)
|
||||||
|
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 0)
|
||||||
|
|
||||||
|
# given permission to approve SRP requests
|
||||||
|
# when 1 pending request
|
||||||
|
# then return 1
|
||||||
|
fleet = SrpFleetMain.objects.create(fleet_time=now())
|
||||||
|
SrpUserRequest.objects.create(
|
||||||
|
killboard_link="https://zkillboard.com/kill/79111612/",
|
||||||
|
srp_status="Pending",
|
||||||
|
srp_fleet_main=fleet,
|
||||||
|
)
|
||||||
|
SrpUserRequest.objects.create(
|
||||||
|
killboard_link="https://zkillboard.com/kill/79111612/",
|
||||||
|
srp_status="Approved",
|
||||||
|
srp_fleet_main=fleet,
|
||||||
|
)
|
||||||
|
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 1)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Import the fonts from CDN
|
// Import the fonts from CDN
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings';
|
||||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot');
|
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot');
|
||||||
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
||||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
||||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
|
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff') format('woff'),
|
||||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
||||||
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
|
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// To build a new CSS file you need to `npm install -g less less-plugin-clean-css`
|
// To build a new CSS file you need to `npm install -g less less-plugin-clean-css`
|
||||||
// Then `lessc --clean-css darkly.less darkly.min.css`
|
// Then `lessc --clean-css darkly.less darkly.min.css`
|
||||||
|
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/bower_components/bootstrap/less/bootstrap.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less";
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/darkly/variables.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/darkly/variables.less";
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/darkly/bootswatch.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/darkly/bootswatch.less";
|
||||||
@import "../bootstrap-locals.less";
|
@import "../bootstrap-locals.less";
|
||||||
@import "../flatly-shared.less";
|
@import "../flatly-shared.less";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,9 +2,9 @@
|
|||||||
// To build a new CSS file you need to `npm install -g less less-plugin-clean-css`
|
// To build a new CSS file you need to `npm install -g less less-plugin-clean-css`
|
||||||
// Then `lessc --clean-css flatly.less flatly.min.css`
|
// Then `lessc --clean-css flatly.less flatly.min.css`
|
||||||
|
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/bower_components/bootstrap/less/bootstrap.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less";
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/flatly/variables.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/flatly/variables.less";
|
||||||
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/flatly/bootswatch.less";
|
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/flatly/bootswatch.less";
|
||||||
@import "../bootstrap-locals.less";
|
@import "../bootstrap-locals.less";
|
||||||
@import "../flatly-shared.less";
|
@import "../flatly-shared.less";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load navactive %}
|
{% load navactive %}
|
||||||
{% load menu_items %}
|
{% load menu_items %}
|
||||||
{% load groupmanagement %}
|
|
||||||
|
|
||||||
<div class="col-sm-2 auth-side-navbar" role="navigation">
|
<div class="col-sm-2 auth-side-navbar" role="navigation">
|
||||||
<div class="collapse navbar-collapse auth-menus-collapse auth-side-navbar-collapse">
|
<div class="collapse navbar-collapse auth-menus-collapse auth-side-navbar-collapse">
|
||||||
@@ -14,19 +13,10 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="{% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
|
<a class="{% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
|
||||||
<i class="fas fa-sitemap fa-fw"></i> {% trans "Groups" %}
|
<i class="fas fa-users fa-fw"></i> {% trans "Groups" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if request.user|can_manage_groups %}
|
|
||||||
<li>
|
|
||||||
<a class="{% navactive request 'groupmanagement:management groupmanagement:membership groupmanagement:membership_list' %}"
|
|
||||||
href="{% url 'groupmanagement:management' %}">
|
|
||||||
<i class="fas fa-sitemap fa-fw"></i> {% trans "Group Management" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% menu_items %}
|
{% menu_items %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
{% if debug %}
|
{% if debug %}
|
||||||
<!-- In template debug, loading less file instead of CSS -->
|
<!-- In template debug, loading less file instead of CSS -->
|
||||||
<link rel="stylesheet/less" type="text/css" href="{% static 'css/themes/darkly/darkly.less' %}" />
|
<link rel="stylesheet/less" type="text/css" href="{% static 'css/themes/darkly/darkly.less' %}" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.3/less.min.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<link rel="stylesheet" href="{% static 'css/themes/darkly/darkly.min.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'css/themes/darkly/darkly.min.css' %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if debug %}
|
{% if debug %}
|
||||||
<!-- In template debug, loading less file instead of CSS -->
|
<!-- In template debug, loading less file instead of CSS -->
|
||||||
<link rel="stylesheet/less" type="text/css" href="{% static 'css/themes/flatly/flatly.less' %}" />
|
<link rel="stylesheet/less" type="text/css" href="{% static 'css/themes/flatly/flatly.less' %}" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.3/less.min.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<link rel="stylesheet" href="{% static 'css/themes/flatly/flatly.min.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'css/themes/flatly/flatly.min.css' %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- End Bootstrap CSS -->
|
<!-- End Bootstrap CSS -->
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{% load static %}
|
<!-- Start Bootstrap + jQuery js from cdnjs -->
|
||||||
<!-- Start Bootstrap + jQuery js -->
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||||
|
<!-- End Bootstrap + jQuery js from cdnjs -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
|
||||||
<!-- End Bootstrap + jQuery js -->
|
|
||||||
3
allianceauth/templates/bundles/clipboard-js.html
Normal file
3
allianceauth/templates/bundles/clipboard-js.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<!-- Start Clipboard.js js from cdnjs -->
|
||||||
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.6/clipboard.min.js"></script>
|
||||||
|
<!-- End Clipboard.js js from cdnjs -->
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<!-- Start DataTables-css -->
|
<!-- Start Datatables-css from cdnjs -->
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.15/css/dataTables.bootstrap.min.css"/>
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/dataTables.bootstrap.min.css"/>
|
||||||
<!-- End DataTables-css -->
|
<!-- End Datatables-css from cdnjs -->
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- Start DataTables-js -->
|
<!-- Start Datatables-js from cdnjs -->
|
||||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.15/js/jquery.dataTables.min.js"></script>
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js"></script>
|
||||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.15/js/dataTables.bootstrap.min.js"></script>
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/dataTables.bootstrap.min.js"></script>
|
||||||
<!-- End DataTables-js -->
|
<!-- End Datatables-js from cdnjs -->
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load staticfiles %}
|
<!-- Start FontAwesome CSS from cdnjs -->
|
||||||
<!-- Font Awesome Bundle -->
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css"/>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" rel="stylesheet" type="text/css">
|
<!-- End FontAwesome CSS from cdnjs -->
|
||||||
<!-- End Font Awesome Bundle -->
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load static %}
|
<!-- Start jQuery-DateTimePicker CSS from cdnjs -->
|
||||||
<!-- Start jQuery datetimepicker CSS -->
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css"/>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.3.7/jquery.datetimepicker.min.css" rel="stylesheet" type="text/css">
|
<!-- End jQuery-DateTimePicker CSS from cdnjs -->
|
||||||
<!-- End jQuery datetimepicker CSS -->
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load static %}
|
<!-- Start jQuery-DateTimePicker JS from cdnjs -->
|
||||||
<!-- Start jQuery datetimepicker js -->
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.3.7/jquery.datetimepicker.min.js"></script>
|
<!-- End jQuery-DateTimePicker JS from cdnjs -->
|
||||||
<!-- End jQuery datetimepicker js -->
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.min.js"></script>
|
<!-- Start Moment.js from cdnjs -->
|
||||||
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js"></script>
|
||||||
{% if locale and LANGUAGE_CODE != 'en' %}
|
{% if locale and LANGUAGE_CODE != 'en' %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/locale/{{ LANGUAGE_CODE }}.js"></script>
|
<!-- Moment.JS Not EN-en -->
|
||||||
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/locale/{{ LANGUAGE_CODE }}.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!-- End Moment JS from cdnjs -->
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load static %}
|
<!-- Start X-editable JS from cdnjs -->
|
||||||
<!-- Start X-Editablle js -->
|
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/js/bootstrap-editable.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/js/bootstrap-editable.min.js"></script>
|
<!-- End X-editable JS from cdnjs -->
|
||||||
<!-- End X-Editable js -->
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load staticfiles %}
|
<!-- Start X-editable CSS from cdnjs -->
|
||||||
<!-- X-Editable Core CSS -->
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css"/>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css" rel="stylesheet">
|
<!-- End X-editable CSS from cdnjs -->
|
||||||
<!-- End Bootstrap CSS -->
|
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
|
|
||||||
{% include 'bundles/moment-js.html' with locale=True %}
|
{% include 'bundles/moment-js.html' with locale=True %}
|
||||||
<script src="{% static 'js/timers.js' %}"></script>
|
<script src="{% static 'js/timers.js' %}"></script>
|
||||||
<script type="text/javascript">
|
<script type="application/javascript">
|
||||||
var locale = "{{ LANGUAGE_CODE }}";
|
var locale = "{{ LANGUAGE_CODE }}";
|
||||||
|
|
||||||
var timers = [
|
var timers = [
|
||||||
|
|||||||
@@ -4,24 +4,58 @@ The menu hooks allow you to dynamically specify menu items from your plugin app
|
|||||||
|
|
||||||
To register a MenuItemHook class you would do the following:
|
To register a MenuItemHook class you would do the following:
|
||||||
|
|
||||||
|
```Python
|
||||||
@hooks.register('menu_item_hook')
|
@hooks.register('menu_item_hook')
|
||||||
def register_menu():
|
def register_menu():
|
||||||
return MenuItemHook('Example Item', 'glyphicon glyphicon-heart', 'example_url_name',150)
|
return MenuItemHook('Example Item', 'glyphicon glyphicon-heart', 'example_url_name',150)
|
||||||
|
```
|
||||||
|
|
||||||
The `MenuItemHook` class specifies some parameters/instance variables required for menu item display.
|
The `MenuItemHook` class specifies some parameters/instance variables required for menu item display.
|
||||||
|
|
||||||
`MenuItemHook(text, classes, url_name, order=None)`
|
## MenuItemHook(text, classes, url_name, order=None)
|
||||||
|
|
||||||
|
### text
|
||||||
|
|
||||||
|
The text shown as menu item, e.g. usually the name of the app.
|
||||||
|
|
||||||
|
### classes
|
||||||
|
|
||||||
#### text
|
|
||||||
The text value of the link
|
|
||||||
#### classes
|
|
||||||
The classes that should be applied to the bootstrap menu item icon
|
The classes that should be applied to the bootstrap menu item icon
|
||||||
#### url_name
|
|
||||||
|
### url_name
|
||||||
|
|
||||||
The name of the Django URL to use
|
The name of the Django URL to use
|
||||||
#### order
|
|
||||||
An integer which specifies the order of the menu item, lowest to highest
|
### order
|
||||||
#### navactive
|
|
||||||
|
An integer which specifies the order of the menu item, lowest to highest. Community apps are free ot use an oder above `1000`. Numbers below are served for Auth.
|
||||||
|
|
||||||
|
### navactive
|
||||||
|
|
||||||
A list of views or namespaces the link should be highlighted on. See [django-navhelper](https://github.com/geelweb/django-navhelper#navactive) for usage. Defaults to the supplied `url_name`.
|
A list of views or namespaces the link should be highlighted on. See [django-navhelper](https://github.com/geelweb/django-navhelper#navactive) for usage. Defaults to the supplied `url_name`.
|
||||||
|
|
||||||
|
### count
|
||||||
|
|
||||||
|
`count` is an integer shown next to the menu item as badge when `count` is not `None`.
|
||||||
|
|
||||||
|
This is a great feature to signal the user, that he has some open issues to take care of within an app. For example Auth uses this feature to show the specific number of open group request to the current user.
|
||||||
|
|
||||||
|
```eval_rst
|
||||||
|
.. hint::
|
||||||
|
Here is how to stay consistent with the Auth design philosophy for using this feature:
|
||||||
|
1. Use it to display open items that the current user can close by himself only. Do not use it for items, that the user has no control over.
|
||||||
|
2. If there are currently no open items, do not show a badge at all.
|
||||||
|
```
|
||||||
|
|
||||||
|
To use it set count the `render()` function of your subclass in accordance to the current user. Here is an example:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
def render(self, request):
|
||||||
|
# ...
|
||||||
|
self.count = calculate_count_for_user(request.user)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
If you cannot get the menu item to look the way you wish, you are free to subclass and override the default render function and the template used.
|
If you cannot get the menu item to look the way you wish, you are free to subclass and override the default render function and the template used.
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
|
|||||||
|
|
||||||
### Creating a Server
|
### Creating a Server
|
||||||
|
|
||||||
Navigate to the [Discord site](https://discordapp.com/) and register an account, or log in if you have one already.
|
Navigate to the [Discord site](https://discord.com/) and register an account, or log in if you have one already.
|
||||||
|
|
||||||
On the left side of the screen you’ll see a circle with a plus sign. This is the button to create a new server. Go ahead and do that, naming it something obvious.
|
On the left side of the screen you’ll see a circle with a plus sign. This is the button to create a new server. Go ahead and do that, naming it something obvious.
|
||||||
|
|
||||||
Now retrieve the server ID [following this procedure.](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-)
|
Now retrieve the server ID [following this procedure.](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-)
|
||||||
|
|
||||||
Update your auth project's settings file, inputting the server ID as `DISCORD_GUILD_ID`
|
Update your auth project's settings file, inputting the server ID as `DISCORD_GUILD_ID`
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Update your auth project's settings file, inputting the server ID as `DISCORD_GU
|
|||||||
|
|
||||||
### Registering an Application
|
### Registering an Application
|
||||||
|
|
||||||
Navigate to the [Discord Developers site.](https://discordapp.com/developers/applications/me) Press the plus sign to create a new application.
|
Navigate to the [Discord Developers site.](https://discord.com/developers/applications/me) Press the plus sign to create a new application.
|
||||||
|
|
||||||
Give it a name and description relating to your auth site. Add a redirect to `https://example.com/discord/callback/`, substituting your domain. Press Create Application.
|
Give it a name and description relating to your auth site. Add a redirect to `https://example.com/discord/callback/`, substituting your domain. Press Create Application.
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Once created, navigate to the services page of your Alliance Auth install as the
|
|||||||
|
|
||||||
This adds a new user to your Discord server with a `BOT` tag, and a new role with the same name as your Discord application. Don't touch either of these. If for some reason the bot loses permissions or is removed from the server, click this button again.
|
This adds a new user to your Discord server with a `BOT` tag, and a new role with the same name as your Discord application. Don't touch either of these. If for some reason the bot loses permissions or is removed from the server, click this button again.
|
||||||
|
|
||||||
To manage roles, this bot role must be at the top of the hierarchy. Edit your Discord server, roles, and click and drag the role with the same name as your application to the top of the list. This role must stay at the top of the list for the bot to work. Finally, the owner of the bot account must enable 2 Factor Authentication (this is required from Discord for kicking and modifying member roles). If you are unsure what 2FA is or how to set it up, refer to [this support page](https://support.discordapp.com/hc/en-us/articles/219576828). It is also recommended to force 2FA on your server (this forces any admins or moderators to have 2fa enabled to perform similar functions on discord).
|
To manage roles, this bot role must be at the top of the hierarchy. Edit your Discord server, roles, and click and drag the role with the same name as your application to the top of the list. This role must stay at the top of the list for the bot to work. Finally, the owner of the bot account must enable 2 Factor Authentication (this is required from Discord for kicking and modifying member roles). If you are unsure what 2FA is or how to set it up, refer to [this support page](https://support.discord.com/hc/en-us/articles/219576828). It is also recommended to force 2FA on your server (this forces any admins or moderators to have 2fa enabled to perform similar functions on discord).
|
||||||
|
|
||||||
Note that the bot will never appear online as it does not participate in chat channels.
|
Note that the bot will never appear online as it does not participate in chat channels.
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,48 @@ On a freshly installed mumble server only your superuser has the right to config
|
|||||||
- user: `SuperUser`
|
- user: `SuperUser`
|
||||||
- password: *what you defined when configuring your mumble server*
|
- password: *what you defined when configuring your mumble server*
|
||||||
|
|
||||||
|
## Optimizing a Mumble Server
|
||||||
|
|
||||||
|
The needs and available resources will vary between Alliance Auth installations. Consider yours when applying these settings.
|
||||||
|
|
||||||
|
### Bandwidth
|
||||||
|
|
||||||
|
<https://wiki.mumble.info/wiki/Murmur.ini#bandwidth>
|
||||||
|
This is likely the most important setting for scaling a Mumble install, The default maximum Bandwidth is 72000bps Per User. Reducing this value will cause your clients to automatically scale back their bandwidth transmitted, while causing a reduction in voice quality. A value thats still high may cause robotic voices or users with bad connections to drop due entirely due to network load.
|
||||||
|
|
||||||
|
Please tune this value to your individual needs, the below scale may provide a rough starting point.
|
||||||
|
72000 - Superior voice quality - Less than 50 users.
|
||||||
|
54000 - No noticeable reduction in quality - 50+ Users or many channels with active audio.
|
||||||
|
36000 - Mild reduction in quality - 100+ Users
|
||||||
|
30000 - Noticeable reduction in quality but not function - 250+ Users
|
||||||
|
|
||||||
|
### Forcing Opus
|
||||||
|
|
||||||
|
<https://wiki.mumble.info/wiki/Murmur.ini#opusthreshold>
|
||||||
|
A Mumble server by default, will fall back to the older CELT codec as soon as a single user connects with an old client. This will significantly reduce your audio quality and likely place higher load on your server. We _highly_ reccommend setting this to Zero, to force OPUS to be used at all times. Be aware any users with Mumble clients prior to 1.2.4 (From 2013...) Will not hear any audio.
|
||||||
|
|
||||||
|
`opusthreshold=0`
|
||||||
|
|
||||||
|
### AutoBan and Rate Limiting
|
||||||
|
|
||||||
|
<https://wiki.mumble.info/wiki/Murmur.ini#autobanAttempts.2C_autobanTimeframe_and_autobanTime>
|
||||||
|
The AutoBan feature has some sensible settings by default, You may wish to tune these if your users keep locking themselves out by opening two clients by mistake, or if you are receiving unwanted attention
|
||||||
|
|
||||||
|
<https://wiki.mumble.info/wiki/Murmur.ini#messagelimit_and_messageburst>
|
||||||
|
This too, is set to a sensible configuration by default. Take note on upgrading older installs, as this may actually be set too restrictively and will rate-limit your admins accidentally, take note of the configuration in <https://github.com/mumble-voip/mumble/blob/master/scripts/murmur.ini#L156>
|
||||||
|
|
||||||
|
### "Suggest" Options
|
||||||
|
|
||||||
|
There is no way to force your users to update their clients or use Push to Talk, but these options will throw an error into their Mumble Client.
|
||||||
|
|
||||||
|
<https://wiki.mumble.info/wiki/Murmur.ini#Miscellany>
|
||||||
|
|
||||||
|
We suggest using Mumble 1.3.0+ for your server and Clients, you can tune this to the latest Patch version.
|
||||||
|
`suggestVersion=1.3.0`
|
||||||
|
|
||||||
|
If Push to Talk is to your tastes, configure the suggestion as follows
|
||||||
|
`suggestPushToTalk=true`
|
||||||
|
|
||||||
## General notes
|
## General notes
|
||||||
|
|
||||||
### Setting a server password
|
### Setting a server password
|
||||||
@@ -238,6 +280,7 @@ Save the file and restart your mumble server afterwards.
|
|||||||
service mumble-server restart
|
service mumble-server restart
|
||||||
```
|
```
|
||||||
|
|
||||||
From now on, only registerd member can join your mumble server. Now if you still want to allow guests to join you have 2 options.
|
From now on, only registered member can join your mumble server. Now if you still want to allow guests to join you have 2 options.
|
||||||
|
|
||||||
- Allow the "Guest" state to activate the Mumble service in your Auth instance
|
- Allow the "Guest" state to activate the Mumble service in your Auth instance
|
||||||
- Use [Mumble temporary links](https://github.com/pvyParts/allianceauth-mumble-temp)
|
- Use [Mumble temporary links](https://github.com/pvyParts/allianceauth-mumble-temp)
|
||||||
Reference in New Issue
Block a user