Compare commits

..

25 Commits

Author SHA1 Message Date
Ariel Rin
3874aa6fee Version Bump 2.8.0a1 2020-09-11 11:52:20 +00:00
Ariel Rin
103e9f3a11 Merge branch 'discourse_beta' into 'master'
Discourse API with external package

See merge request allianceauth/allianceauth!1251
2020-09-11 11:33:19 +00:00
Ariel Rin
d02c25f421 Merge branch 'feature_menu_item_badges' into 'master'
Add menu item badge feature and update group icons

See merge request allianceauth/allianceauth!1240
2020-09-11 11:33:14 +00:00
Erik Kalkoken
228af38a4a Add menu item badge feature and update group icons 2020-09-11 11:33:14 +00:00
AaronKable
051a48885c discourse API with external package 2020-09-11 17:19:54 +08:00
Ariel Rin
6bcdc6052f Merge branch 'local-delivery' into 'master'
JS/CSS/Font Refactoring for use with AA-GDPR Package

Closes #1217

See merge request allianceauth/allianceauth!1247
2020-09-11 04:13:01 +00:00
Ariel Rin
af3527e64f Revert "load bootswatch less locally #1217"
This reverts commit 3a9a7267ea8734ba0a5cf1d0fea632eb2276d45c.
2020-09-11 04:13:01 +00:00
Ariel Rin
17ef3dd07a Merge branch 'srp_fix' into 'master'
Use request.scheme to get the http/https for the site

See merge request allianceauth/allianceauth!1249
2020-09-11 03:05:06 +00:00
AaronKable
1f165ecd2a use request.scheme to get the http/https for the site 2020-09-07 19:10:58 +08:00
Ariel Rin
70d1d450a9 Add Korean and Russian as features 2020-09-03 12:24:23 +00:00
Ariel Rin
b667892698 Version Bump 2.7.5 2020-09-01 02:09:05 +00:00
Ariel Rin
dc11add0e9 Merge branch 'discordapp.com-deprecation' into 'master'
discord.com replaces discordapp.com

See merge request allianceauth/allianceauth!1248
2020-09-01 01:53:25 +00:00
Ariel Rin
cb429a0b88 discord.com replaces discordapp.com 2020-09-01 11:20:57 +10:00
Ariel Rin
b51039cfc0 Merge branch 'docs' into 'master'
Add Optimizing Mumble to Services docs

See merge request allianceauth/allianceauth!1243
2020-09-01 01:11:49 +00:00
Ariel Rin
eadd959d95 Merge branch 'fix_celery_once_backend' into 'master'
Fix celery once not working properly

See merge request allianceauth/allianceauth!1246
2020-08-25 06:13:59 +00:00
Erik Kalkoken
1856e03d88 Fix celery once not working properly 2020-08-25 06:13:59 +00:00
Ariel Rin
7dcfa622a3 Merge branch 'issue_1234_exiom' into 'master'
Month Ordering Fix for Group Management Audit Logs

See merge request allianceauth/allianceauth!1245
2020-08-25 06:07:45 +00:00
Exiom
64251b9b3c Fix Date/Time Month Ordering #1234 2020-08-21 03:57:24 +00:00
Exiom
6b073dd5fc Fix Date/Time Month Ordering #1234 2020-08-21 03:33:35 +00:00
Ariel Rin
0911fabfb2 Merge branch 'issue_1234' into 'master'
Correct Month Ordering in Group Management Audit Logs

Closes #1234

See merge request allianceauth/allianceauth!1244
2020-08-20 07:11:47 +00:00
Ariel Rin
050d3f5e63 use month numerical 2020-08-20 16:55:18 +10:00
Ariel Rin
bbe3f78ad1 Grammar and Spelling Corrections 2020-08-20 15:19:50 +10:00
Ariel Rin
8204c18895 Add Optimzing Mumble 2020-08-20 15:09:07 +10:00
Ariel Rin
b91c788897 Merge branch 'bulk-affiliations' into 'master'
Reduce run time for eve online model updates

See merge request allianceauth/allianceauth!1227
2020-08-20 03:14:40 +00:00
Erik Kalkoken
1d20a3029f Only update characters if they have changed corp or alliance by bulk calling affiliations before calling character tasks. 2020-08-20 03:14:40 +00:00
59 changed files with 937 additions and 436 deletions

View File

@@ -36,7 +36,7 @@ Main features:
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
- Chinese :cn:, English :us:, German :de: and Spanish :es: localization
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).

View File

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

View File

@@ -5,35 +5,96 @@ from .models import EveAllianceInfo
from .models import EveCharacter
from .models import EveCorporationInfo
from . import providers
logger = logging.getLogger(__name__)
TASK_PRIORITY = 7
CHUNK_SIZE = 500
def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]
@shared_task
def update_corp(corp_id):
"""Update given corporation from ESI"""
EveCorporationInfo.objects.update_corporation(corp_id)
@shared_task
def update_alliance(alliance_id):
"""Update given alliance from ESI"""
EveAllianceInfo.objects.update_alliance(alliance_id).populate_alliance()
@shared_task
def update_character(character_id):
"""Update given character from ESI"""
EveCharacter.objects.update_character(character_id)
@shared_task
def run_model_update():
"""Update all alliances, corporations and characters from ESI"""
# update existing corp models
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)
update_corp.apply_async(
args=[corp['corporation_id']], priority=TASK_PRIORITY
)
# update existing alliance models
for alliance in EveAllianceInfo.objects.all().values('alliance_id'):
update_alliance.apply_async(args=[alliance['alliance_id']], priority=TASK_PRIORITY)
update_alliance.apply_async(
args=[alliance['alliance_id']], priority=TASK_PRIORITY
)
#update existing character models
for character in EveCharacter.objects.all().values('character_id'):
update_character.apply_async(args=[character['character_id']], priority=TASK_PRIORITY)
# update existing character models
character_ids = EveCharacter.objects.all().values_list('character_id', flat=True)
for character_ids_chunk in chunks(character_ids, CHUNK_SIZE):
affiliations_raw = providers.provider.client.Character\
.post_characters_affiliation(characters=character_ids_chunk).result()
character_names = providers.provider.client.Universe\
.post_universe_names(ids=character_ids_chunk).result()
affiliations = {
affiliation.get('character_id'): affiliation
for affiliation in affiliations_raw
}
# add character names to affiliations
for character in character_names:
character_id = character.get('id')
if character_id in affiliations:
affiliations[character_id]['name'] = character.get('name')
# fetch current characters
characters = EveCharacter.objects.filter(character_id__in=character_ids_chunk)\
.values('character_id', 'corporation_id', 'alliance_id', 'character_name')
for character in characters:
character_id = character.get('character_id')
if character_id in affiliations:
affiliation = affiliations[character_id]
corp_changed = (
character.get('corporation_id') != affiliation.get('corporation_id')
)
alliance_id = character.get('alliance_id')
if not alliance_id:
alliance_id = None
alliance_changed = alliance_id != affiliation.get('alliance_id')
name_changed = False
fetched_name = affiliation.get('name', False)
if fetched_name:
name_changed = character.get('character_name') != fetched_name
if corp_changed or alliance_changed or name_changed:
update_character.apply_async(
args=[character.get('character_id')], priority=TASK_PRIORITY
)

View File

@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import patch, Mock
from django.test import TestCase
@@ -44,55 +44,202 @@ class TestTasks(TestCase):
mock_EveCharacter.objects.update_character.call_args[0][0], 42
)
@patch('allianceauth.eveonline.tasks.update_character')
@patch('allianceauth.eveonline.tasks.update_alliance')
@patch('allianceauth.eveonline.tasks.update_corp')
def test_run_model_update(
self,
mock_update_corp,
mock_update_alliance,
mock_update_character,
):
@patch('allianceauth.eveonline.tasks.update_character')
@patch('allianceauth.eveonline.tasks.update_alliance')
@patch('allianceauth.eveonline.tasks.update_corp')
@patch('allianceauth.eveonline.providers.provider')
@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2)
class TestRunModelUpdate(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
EveCorporationInfo.objects.all().delete()
EveAllianceInfo.objects.all().delete()
EveCharacter.objects.all().delete()
EveCorporationInfo.objects.create(
corporation_id=2345,
corporation_name='corp.name',
corporation_ticker='corp.ticker',
corporation_ticker='c.c.t',
member_count=10,
alliance=None,
)
EveAllianceInfo.objects.create(
alliance_id=3456,
alliance_name='alliance.name',
alliance_ticker='alliance.ticker',
executor_corp_id='78910',
alliance_ticker='a.t',
executor_corp_id=5,
)
EveCharacter.objects.create(
character_id=1,
character_name='character.name1',
corporation_id=2345,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=None
)
EveCharacter.objects.create(
character_id=1234,
character_name='character.name',
corporation_id=2345,
character_id=2,
character_name='character.name2',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
EveCharacter.objects.create(
character_id=3,
character_name='character.name3',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
EveCharacter.objects.create(
character_id=4,
character_name='character.name4',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
"""
EveCharacter.objects.create(
character_id=5,
character_name='character.name5',
corporation_id=9876,
corporation_name='character.corp.name',
corporation_ticker='c.c.t', # max 5 chars
alliance_id=3456,
alliance_name='character.alliance.name',
)
"""
def setUp(self):
self.affiliations = [
{'character_id': 1, 'corporation_id': 5},
{'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456},
{'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456},
{'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456}
]
self.names = [
{'id': 1, 'name': 'character.name1'},
{'id': 2, 'name': 'character.name2'},
{'id': 3, 'name': 'character.name3'},
{'id': 4, 'name': 'character.name4_new'}
]
def test_normal_run(
self,
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
):
def get_affiliations(characters: list):
response = [x for x in self.affiliations if x['character_id'] in characters]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
def get_names(ids: list):
response = [x for x in self.names if x['id'] in ids]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
mock_provider.client.Character.post_characters_affiliation.side_effect \
= get_affiliations
mock_provider.client.Universe.post_universe_names.side_effect = get_names
run_model_update()
self.assertEqual(
mock_provider.client.Character.post_characters_affiliation.call_count, 2
)
self.assertEqual(
mock_provider.client.Universe.post_universe_names.call_count, 2
)
# character 1 has changed corp
# character 2 no change
# character 3 has changed alliance
# character 4 has changed name
self.assertEqual(mock_update_corp.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_corp.apply_async.call_args[1]['args'][0]), 2345
)
self.assertEqual(mock_update_alliance.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456
)
)
characters_updated = {
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
}
excepted = {1, 3, 4}
self.assertSetEqual(characters_updated, excepted)
def test_ignore_character_not_in_affiliations(
self,
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
):
def get_affiliations(characters: list):
response = [x for x in self.affiliations if x['character_id'] in characters]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
def get_names(ids: list):
response = [x for x in self.names if x['id'] in ids]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
del self.affiliations[0]
mock_provider.client.Character.post_characters_affiliation.side_effect \
= get_affiliations
mock_provider.client.Universe.post_universe_names.side_effect = get_names
self.assertEqual(mock_update_character.apply_async.call_count, 1)
self.assertEqual(
int(mock_update_character.apply_async.call_args[1]['args'][0]), 1234
)
run_model_update()
characters_updated = {
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
}
excepted = {3, 4}
self.assertSetEqual(characters_updated, excepted)
def test_ignore_character_not_in_names(
self,
mock_provider,
mock_update_corp,
mock_update_alliance,
mock_update_character,
):
def get_affiliations(characters: list):
response = [x for x in self.affiliations if x['character_id'] in characters]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
def get_names(ids: list):
response = [x for x in self.names if x['id'] in ids]
mock_operator = Mock(**{'result.return_value': response})
return mock_operator
del self.names[3]
mock_provider.client.Character.post_characters_affiliation.side_effect \
= get_affiliations
mock_provider.client.Universe.post_universe_names.side_effect = get_names
run_model_update()
characters_updated = {
x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list
}
excepted = {1, 3}
self.assertSetEqual(characters_updated, excepted)

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

View File

@@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, User
from django.db.models import Q, QuerySet
from allianceauth.authentication.models import State
from .models import GroupRequest
logger = logging.getLogger(__name__)
@@ -101,3 +102,18 @@ class GroupManager:
if user.is_authenticated:
return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
return False
@classmethod
def pending_requests_count_for_user(cls, user: User) -> int:
"""Returns the number of pending group requests for the given user"""
if cls.has_management_permission(user):
return GroupRequest.objects.filter(status="pending").count()
else:
return (
GroupRequest.objects
.filter(status="pending")
.filter(group__authgroup__group_leaders__exact=user)
.select_related("group__authgroup__group_leaders")
.count()
)

View File

@@ -4,7 +4,6 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from allianceauth.authentication.models import State
from datetime import datetime
class GroupRequest(models.Model):

View File

@@ -33,7 +33,7 @@
<tbody>
{% for entry in entries %}
<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.req_char }}</td>
<td class="text-center">{{ entry.req_char.corporation_name }}</td>
@@ -66,7 +66,8 @@
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% include 'bundles/moment-js.html' with locale=True %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %}
{% block extra_css %}
@@ -74,7 +75,26 @@
{% endblock %}
{% block extra_script %}
$.fn.dataTable.moment = function ( format, locale ) {
var types = $.fn.dataTable.ext.type;
// Add type detection
types.detect.unshift( function ( d ) {
return moment( d, format, locale, true ).isValid() ?
'moment-'+format :
null;
} );
// Add sorting method - use an integer for the sorting
types.order[ 'moment-'+format+'-pre' ] = function ( d ) {
return moment( d, format, locale, true ).unix();
};
};
$(document).ready(function(){
$.fn.dataTable.moment( 'YYYY-MMM-D, HH:mm' );
$('#log-entries').DataTable({
order: [[ 0, 'desc' ], [ 1, 'asc' ] ],
filterDropDown:

View File

@@ -73,7 +73,7 @@
</div>
{% endblock content %}
{% block extra_javascript %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
{% include 'bundles/clipboard-js.html' %}
<script>
new ClipboardJS('#clipboard-copy');
</script>

View File

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

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils
from ..models import AuthGroup
from ..models import GroupRequest
from ..managers import GroupManager
@@ -15,6 +15,7 @@ class MockUserNotAuthenticated():
def __init__(self):
self.is_authenticated = False
class GroupManagementVisibilityTestCase(TestCase):
@classmethod
def setUpTestData(cls):
@@ -37,22 +38,20 @@ class GroupManagementVisibilityTestCase(TestCase):
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_get_group_leaders_groups(self):
self.group1.authgroup.group_leaders.add(self.user)
self.group2.authgroup.group_leader_groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all
self.assertIn(self.group1, groups) #avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all
self.user.groups.add(self.group1)
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
def test_can_manage_group(self):
self.group1.authgroup.group_leaders.add(self.user)
self.user.groups.add(self.group1)
@@ -182,7 +181,6 @@ class TestGroupManager(TestCase):
]:
self.assertFalse(GroupManager.joinable_group(x, member_state))
def test_joinable_group_guest(self):
guest_state = AuthUtils.get_guest_state()
for x in [
@@ -200,7 +198,6 @@ class TestGroupManager(TestCase):
]:
self.assertFalse(GroupManager.joinable_group(x, guest_state))
def test_get_all_non_internal_groups(self):
result = GroupManager.get_all_non_internal_groups()
expected = {
@@ -224,7 +221,7 @@ class TestGroupManager(TestCase):
def test_get_joinable_groups_for_user_no_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_guest_state())
result = GroupManager.get_joinable_groups_for_user(self.user)
expected= {self.group_public_1, self.group_public_2}
expected = {self.group_public_1, self.group_public_2}
self.assertSetEqual(set(result), expected)
def test_get_joinable_groups_for_user_guest_w_permission_(self):
@@ -335,3 +332,96 @@ class TestGroupManager(TestCase):
self.assertFalse(
GroupManager.can_manage_group(user, self.group_default)
)
class TestPendingRequestsCountForUser(TestCase):
def setUp(self) -> None:
self.group_1 = Group.objects.create(name="Group 1")
self.group_2 = Group.objects.create(name="Group 2")
self.user_leader_1 = AuthUtils.create_member('Clark Kent')
self.group_1.authgroup.group_leaders.add(self.user_leader_1)
self.user_leader_2 = AuthUtils.create_member('Peter Parker')
self.group_2.authgroup.group_leaders.add(self.user_leader_2)
self.user_requestor = AuthUtils.create_member('Bruce Wayne')
def test_single_request_for_leader(self):
# given user_leader_1 is leader of group_1
# and user_leader_2 is leader of group_2
# when user_requestor is requesting access to group 1
# then return 1 for user_leader 1 and 0 for user_leader_2
GroupRequest.objects.create(
status="pending", user=self.user_requestor, group=self.group_1
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_2), 0
)
def test_return_none_for_none_leader(self):
# given user_requestor is leader of no group
# when user_requestor is requesting access to group 1
# then return 0 for user_requestor
GroupRequest.objects.create(
status="pending", user=self.user_requestor, group=self.group_1
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_requestor), 0
)
def test_single_leave_request(self):
# given user_leader_2 is leader of group_2
# and user_requestor is member of group 2
# when user_requestor is requesting to leave group 2
# then return 1 for user_leader_2
self.user_requestor.groups.add(self.group_2)
GroupRequest.objects.create(
status="pending",
user=self.user_requestor,
group=self.group_2,
leave_request=True
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_2), 1
)
def test_join_and_leave_request(self):
# given user_leader_2 is leader of group_2
# and user_requestor is member of group 2
# when user_requestor is requesting to leave group 2
# and user_requestor_2 is requesting to join group 2
# then return 2 for user_leader_2
self.user_requestor.groups.add(self.group_2)
user_requestor_2 = AuthUtils.create_member("Lex Luther")
GroupRequest.objects.create(
status="pending",
user=user_requestor_2,
group=self.group_2
)
GroupRequest.objects.create(
status="pending",
user=self.user_requestor,
group=self.group_2,
leave_request=True
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_2), 2
)
def test_single_request_for_user_with_management_perm(self):
# given user_leader_4 which is leafer of no group
# but has the management permissions
# when user_requestor is requesting access to group 1
# then return 1 for user_leader_4
user_leader_4 = AuthUtils.create_member("Lex Luther")
AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4)
user_leader_4 = User.objects.get(pk=user_leader_4.pk)
GroupRequest.objects.create(
status="pending", user=self.user_requestor, group=self.group_1
)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
)

View File

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

View File

@@ -1,32 +1,29 @@
from . import views
from django.conf.urls import include, url
from django.conf.urls import url
app_name = 'groupmanagement'
urlpatterns = [
url(r'^groups/', views.groups_view, name='groups'),
url(r'^group/', include([
url(r'^management/', views.group_management,
name='management'),
url(r'^membership/$', views.group_membership,
name='membership'),
url(r'^membership/(\w+)/$', views.group_membership_list,
name='membership_list'),
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
name='membership_remove'),
url(r'^request_add/(\w+)', views.group_request_add,
name='request_add'),
url(r'^request/accept/(\w+)', views.group_accept_request,
name='accept_request'),
url(r'^request/reject/(\w+)', views.group_reject_request,
name='reject_request'),
url(r'^request_leave/(\w+)', views.group_request_leave,
name='request_leave'),
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
name='leave_accept_request'),
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
name='leave_reject_request'),
])),
url(r'^groups/', views.groups_view, name='groups'),
url(r'^management/', views.group_management,
name='management'),
url(r'^membership/$', views.group_membership,
name='membership'),
url(r'^membership/(\w+)/$', views.group_membership_list,
name='membership_list'),
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
name='membership_remove'),
url(r'^request_add/(\w+)', views.group_request_add,
name='request_add'),
url(r'^request/accept/(\w+)', views.group_accept_request,
name='accept_request'),
url(r'^request/reject/(\w+)', views.group_reject_request,
name='reject_request'),
url(r'^request_leave/(\w+)', views.group_request_leave,
name='request_leave'),
url(r'leave_request/accept/(\w+)', views.group_leave_accept_request,
name='leave_accept_request'),
url(r'^leave_request/reject/(\w+)', views.group_leave_reject_request,
name='leave_reject_request'),
]

View File

@@ -1,7 +1,10 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks
from allianceauth.hrapplications import urls
from allianceauth.services.hooks import MenuItemHook, UrlHook
from . import urls
from .models import Application
class ApplicationsMenu(MenuItemHook):
@@ -12,6 +15,11 @@ class ApplicationsMenu(MenuItemHook):
'hrapplications:index',
navactive=['hrapplications:'])
def render(self, request):
app_count = Application.objects.pending_requests_count_for_user(request.user)
self.count = app_count if app_count and app_count > 0 else None
return MenuItemHook.render(self, request)
@hooks.register('menu_item_hook')
def register_menu():

View 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

View File

@@ -2,8 +2,9 @@ from django.contrib.auth.models import User
from django.db import models
from sortedm2m.fields import SortedManyToManyField
from allianceauth.eveonline.models import EveCharacter
from allianceauth.eveonline.models import EveCorporationInfo
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
from .managers import ApplicationManager
class ApplicationQuestion(models.Model):
@@ -22,6 +23,7 @@ class ApplicationChoice(models.Model):
def __str__(self):
return self.choice_text
class ApplicationForm(models.Model):
questions = SortedManyToManyField(ApplicationQuestion)
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
@@ -38,6 +40,8 @@ class Application(models.Model):
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
objects = ApplicationManager()
def __str__(self):
return str(self.user) + " Application To " + str(self.form)

View File

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

View File

@@ -36,9 +36,15 @@
{% block extra_script %}
$('#id_start').datetimepicker({
lang: '{{ LANGUAGE_CODE }}',
maskInput: true,
format: 'Y-m-d H:i',minDate:0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -41,7 +41,7 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="text/javascript">
<script type="application/javascript">
// Data
var timers = [
{% for op in optimer %}
@@ -53,7 +53,7 @@
{% endfor %}
];
</script>
<script type="text/javascript">
<script type="application/javascript">
timedUpdate();
setAllLocalTimes();

View File

@@ -44,9 +44,15 @@
{% block extra_script %}
$('#id_start').datetimepicker({
lang: '{{ LANGUAGE_CODE }}',
maskInput: true,
format: 'Y-m-d H:i',minDate:0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -47,7 +47,7 @@
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %}
{% block extra_css %}

View File

@@ -80,7 +80,7 @@
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
<script type="text/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %}
{% block extra_css %}

View File

@@ -139,6 +139,11 @@ class MenuItemHook:
self.url_name = url_name
self.template = 'public/menuitem.html'
self.order = order if order is not None else 9999
# count is an integer shown next to the menu item as badge when count != None
# apps need to set the count in their child class, e.g. in render() method
self.count = None
navactive = navactive or []
navactive.append(url_name)
self.navactive = navactive

View File

@@ -3,7 +3,7 @@ from ..utils import clean_setting
# Base URL for all API calls. Must end with /.
DISCORD_API_BASE_URL = clean_setting(
'DISCORD_API_BASE_URL', 'https://discordapp.com/api/'
'DISCORD_API_BASE_URL', 'https://discord.com/api/'
)
# Low level 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
DISCORD_OAUTH_BASE_URL = clean_setting(
'DISCORD_OAUTH_BASE_URL', 'https://discordapp.com/api/oauth2/authorize'
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
)
# Base authorization URL for Discord Oauth
DISCORD_OAUTH_TOKEN_URL = clean_setting(
'DISCORD_OAUTH_TOKEN_URL', 'https://discordapp.com/api/oauth2/token'
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
)
# How long the Discord guild names retrieved from the server are

View File

@@ -33,7 +33,7 @@ logger = set_logger_to_file(
)
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
API_BASE_URL = 'https://discordapp.com/api/'
API_BASE_URL = 'https://discord.com/api/'
TEST_RETRY_AFTER = 3000

View File

@@ -4,7 +4,7 @@ import re
from django.conf import settings
from django.core.cache import cache
from hashlib import md5
from . import providers
logger = logging.getLogger(__name__)
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
@@ -19,128 +19,8 @@ class DiscourseError(Exception):
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
# not exhaustive, only the ones we need
ENDPOINTS = {
'groups': {
'list': {
'path': "/groups/search.json",
'method': 'get',
'args': {
'required': [],
'optional': [],
},
},
'create': {
'path': "/admin/groups",
'method': 'post',
'args': {
'required': ['name'],
'optional': ['visible'],
}
},
'add_user': {
'path': "/admin/groups/%s/members.json",
'method': 'put',
'args': {
'required': ['usernames'],
'optional': [],
},
},
'remove_user': {
'path': "/admin/groups/%s/members.json",
'method': 'delete',
'args': {
'required': ['username'],
'optional': [],
},
},
'delete': {
'path': "/admin/groups/%s.json",
'method': 'delete',
'args': {
'required': [],
'optional': [],
},
},
},
'users': {
'create': {
'path': "/users",
'method': 'post',
'args': {
'required': ['name', 'email', 'password', 'username'],
'optional': ['active'],
},
},
'update': {
'path': "/users/%s.json",
'method': 'put',
'args': {
'required': ['params'],
'optional': [],
}
},
'get': {
'path': "/users/%s.json",
'method': 'get',
'args': {
'required': [],
'optional': [],
},
},
'activate': {
'path': "/admin/users/%s/activate",
'method': 'put',
'args': {
'required': [],
'optional': [],
},
},
'set_email': {
'path': "/users/%s/preferences/email",
'method': 'put',
'args': {
'required': ['email'],
'optional': [],
},
},
'suspend': {
'path': "/admin/users/%s/suspend",
'method': 'put',
'args': {
'required': ['duration', 'reason'],
'optional': [],
},
},
'unsuspend': {
'path': "/admin/users/%s/unsuspend",
'method': 'put',
'args': {
'required': [],
'optional': [],
},
},
'logout': {
'path': "/admin/users/%s/log_out",
'method': 'post',
'args': {
'required': [],
'optional': [],
},
},
'external': {
'path': "/users/by-external/%s.json",
'method': 'get',
'args': {
'required': [],
'optional': [],
},
},
},
}
class DiscourseManager:
def __init__(self):
pass
@@ -148,55 +28,14 @@ class DiscourseManager:
SUSPEND_DAYS = 99999
SUSPEND_REASON = "Disabled by auth."
@staticmethod
def __exc(endpoint, *args, **kwargs):
params = {
'api_key': settings.DISCOURSE_API_KEY,
'api_username': settings.DISCOURSE_API_USERNAME,
}
silent = kwargs.pop('silent', False)
if args:
endpoint['parsed_url'] = endpoint['path'] % args
else:
endpoint['parsed_url'] = endpoint['path']
data = {}
for arg in endpoint['args']['required']:
data[arg] = kwargs[arg]
for arg in endpoint['args']['optional']:
if arg in kwargs:
data[arg] = kwargs[arg]
for arg in kwargs:
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], headers=params,
json=data)
try:
if 'errors' in r.json() and not silent:
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
raise DiscourseError(endpoint, r.json()['errors'])
if 'success' in r.json():
if not r.json()['success'] and not silent:
raise DiscourseError(endpoint, None)
out = r.json()
except ValueError:
out = r.text
finally:
try:
r.raise_for_status()
except requests.exceptions.HTTPError as e:
raise DiscourseError(endpoint, e.response.status_code)
return out
@staticmethod
def _get_groups():
endpoint = ENDPOINTS['groups']['list']
data = DiscourseManager.__exc(endpoint)
data = providers.discourse.client.groups()
return [g for g in data if not g['automatic']]
@staticmethod
def _create_group(name):
endpoint = ENDPOINTS['groups']['create']
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
return providers.discourse.client.create_group(name=name[:20], visible=True)['basic_group']
@staticmethod
def _generate_cache_group_name_key(name):
@@ -234,13 +73,11 @@ class DiscourseManager:
@staticmethod
def __add_user_to_group(g_id, username):
endpoint = ENDPOINTS['groups']['add_user']
DiscourseManager.__exc(endpoint, g_id, usernames=username)
providers.discourse.client.add_group_member(g_id, username)
@staticmethod
def __remove_user_from_group(g_id, username):
endpoint = ENDPOINTS['groups']['remove_user']
DiscourseManager.__exc(endpoint, g_id, username=username)
def __remove_user_from_group(g_id, uid):
providers.discourse.client.delete_group_member(g_id, uid)
@staticmethod
def __generate_group_dict(names):
@@ -252,39 +89,35 @@ class DiscourseManager:
@staticmethod
def __get_user_groups(username):
data = DiscourseManager.__get_user(username)
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
return [g['id'] for g in data['groups'] if not g['automatic']]
@staticmethod
def __user_name_to_id(name, silent=False):
data = DiscourseManager.__get_user(name, silent=silent)
data = DiscourseManager.__get_user(name)
return data['user']['id']
@staticmethod
def __get_user(username, silent=False):
endpoint = ENDPOINTS['users']['get']
return DiscourseManager.__exc(endpoint, username, silent=silent)
return providers.discourse.client.user(username)
@staticmethod
def __activate_user(username):
endpoint = ENDPOINTS['users']['activate']
u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, u_id)
providers.discourse.client.activate(u_id)
@staticmethod
def __update_user(username, **kwargs):
endpoint = ENDPOINTS['users']['update']
u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
providers.discourse.client.update_user(endpoint, u_id, **kwargs)
@staticmethod
def __create_user(username, email, password):
endpoint = ENDPOINTS['users']['create']
DiscourseManager.__exc(endpoint, name=username, username=username, email=email, password=password, active=True)
providers.discourse.client.create_user(username, username, email, password)
@staticmethod
def __check_if_user_exists(username):
try:
DiscourseManager.__user_name_to_id(username, silent=True)
DiscourseManager.__user_name_to_id(username)
return True
except DiscourseError:
return False
@@ -292,30 +125,26 @@ class DiscourseManager:
@staticmethod
def __suspend_user(username):
u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['suspend']
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
reason=DiscourseManager.SUSPEND_REASON)
return providers.discourse.client.suspend(u_id, DiscourseManager.SUSPEND_DAYS,
DiscourseManager.SUSPEND_REASON)
@staticmethod
def __unsuspend(username):
u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['unsuspend']
return DiscourseManager.__exc(endpoint, u_id)
return providers.discourse.client.unsuspend(u_id)
@staticmethod
def __set_email(username, email):
endpoint = ENDPOINTS['users']['set_email']
return DiscourseManager.__exc(endpoint, username, email=email)
return providers.discourse.client.update_email(username, email)
@staticmethod
def __logout(u_id):
endpoint = ENDPOINTS['users']['logout']
return DiscourseManager.__exc(endpoint, u_id)
return providers.discourse.client.log_out(u_id)
@staticmethod
def __get_user_by_external(u_id):
endpoint = ENDPOINTS['users']['external']
return DiscourseManager.__exc(endpoint, u_id)
data = providers.discourse.client.user_by_external_id(u_id)
return data
@staticmethod
def __user_id_by_external_id(u_id):
@@ -351,7 +180,9 @@ class DiscourseManager:
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
group_dict = DiscourseManager.__generate_group_dict(groups)
inv_group_dict = {v: k for k, v in group_dict.items()}
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
discord_user = DiscourseManager.__get_user_by_external(user.pk)
username = discord_user['username']
uid = discord_user['id']
user_groups = DiscourseManager.__get_user_groups(username)
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
rem_groups = [x for x in user_groups if x not in inv_group_dict]
@@ -364,7 +195,7 @@ class DiscourseManager:
logger.info(
"Updating discourse user %s groups: removing %s" % (username, rem_groups))
for g in rem_groups:
DiscourseManager.__remove_user_from_group(g, username)
DiscourseManager.__remove_user_from_group(g, uid)
@staticmethod
def disable_user(user):

View File

@@ -16,3 +16,4 @@ class DiscourseUser(models.Model):
permissions = (
("access_discourse", u"Can access the Discourse service"),
)

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

View File

@@ -47,7 +47,8 @@ class DiscourseTasks:
logger.debug("Updating discourse groups for user %s" % user)
try:
DiscourseManager.update_groups(user)
except:
except Exception as e:
logger.exception(e)
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
logger.debug("Updated user %s discourse groups." % user)
@@ -63,3 +64,4 @@ class DiscourseTasks:
def get_username(user):
from .auth_hooks import DiscourseService
return NameFormatter(DiscourseService(), user).format_name()

View File

@@ -5,7 +5,6 @@ from django.contrib.auth.models import User
from .hooks import ServicesHook
from celery_once import QueueOnce as BaseTask, AlreadyQueued
from django.core.cache import cache
from time import time
logger = logging.getLogger(__name__)
@@ -22,14 +21,9 @@ class DjangoBackend:
@staticmethod
def raise_or_lock(key, timeout):
now = int(time())
result = cache.get(key)
if result:
remaining = int(result) - now
if remaining > 0:
raise AlreadyQueued(remaining)
else:
cache.set(key, now + timeout, timeout)
acquired = cache.add(key=key, value="lock", timeout=timeout)
if not acquired:
raise AlreadyQueued(int(cache.ttl(key)))
@staticmethod
def clear_lock(key):

View File

@@ -2,7 +2,10 @@
{% load navactive %}
<li>
<a class="{% navactive request item.navactive|join:" " %}" href="{% url item.url_name %}">
<i class="{{ item.classes }}"></i> {% trans item.text %}
<a class="{% navactive request item.navactive|join:' ' %}" href="{% url item.url_name %}">
<i class="{{ item.classes }}"></i> {% trans item.text %}
{% if item.count != None %}
<span class="badge">{{ item.count }}</span>
{% endif %}
</a>
</li>

View File

@@ -1,11 +1,15 @@
from unittest import mock
from celery_once import AlreadyQueued
from django.core.cache import cache
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.services.tasks import validate_services
from ..tasks import DjangoBackend
class ServicesTasksTestCase(TestCase):
def setUp(self):
@@ -24,3 +28,46 @@ class ServicesTasksTestCase(TestCase):
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
class TestDjangoBackend(TestCase):
TEST_KEY = "my-django-backend-test-key"
TIMEOUT = 1800
def setUp(self) -> None:
cache.delete(self.TEST_KEY)
self.backend = DjangoBackend(dict())
def test_can_get_lock(self):
"""
when lock can be acquired
then set it with timetout
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))
self.assertAlmostEqual(cache.ttl(self.TEST_KEY), self.TIMEOUT, delta=2)
def test_when_cant_get_lock_raise_exception(self):
"""
when lock can bot be acquired
then raise AlreadyQueued exception with countdown
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
try:
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
except Exception as ex:
self.assertIsInstance(ex, AlreadyQueued)
self.assertAlmostEqual(ex.countdown, self.TIMEOUT, delta=2)
def test_can_clear_lock(self):
"""
when a lock exists
then can get a new lock after clearing it
"""
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.backend.clear_lock(self.TEST_KEY)
self.backend.raise_or_lock(self.TEST_KEY, self.TIMEOUT)
self.assertIsNotNone(cache.get(self.TEST_KEY))

View File

@@ -1,7 +1,10 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook
from . import urls
from .managers import SRPManager
class SrpMenu(MenuItemHook):
@@ -13,6 +16,8 @@ class SrpMenu(MenuItemHook):
def render(self, request):
if request.user.has_perm('srp.access_srp'):
app_count = SRPManager.pending_requests_count_for_user(request.user)
self.count = app_count if app_count and app_count > 0 else None
return MenuItemHook.render(self, request)
return ''

View File

@@ -1,11 +1,13 @@
import logging
import os
import requests
from django.contrib.auth.models import User
from allianceauth import NAME
from allianceauth.eveonline.providers import provider
from .models import SrpUserRequest
logger = logging.getLogger(__name__)
@@ -50,3 +52,12 @@ class SRPManager:
return ship_type, ship_value, victim_id
else:
raise ValueError("Invalid Kill ID or Hash.")
@staticmethod
def pending_requests_count_for_user(user: User):
"""returns the number of open SRP requests for given user
or None if user has no permission"""
if user.has_perm("auth.srp_management"):
return SrpUserRequest.objects.filter(srp_status="pending").count()
else:
return None

View File

@@ -26,7 +26,7 @@
{% else %}
<div class="alert alert-info" role="alert">{% blocktrans %}Give this link to the line members{% endblocktrans %}.</div>
<div class="alert alert-info" role="alert">
http://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
{{ request.scheme }}://{{ request.get_host }}{% url 'srp:request' completed_srp_code %}</div>
<div class="text-center">
<a href="{% url 'srp:management' %}" class="btn btn-primary btn-lg">{% trans "Continue" %}</a>
</div>
@@ -34,7 +34,6 @@
</div>
</div>
</div>
</div>
{% endblock content %}
@@ -46,8 +45,15 @@
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
maskInput: true,
format: 'Y-m-d H:i'
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -1,11 +1,16 @@
import inspect
import json
import os
from unittest.mock import patch, Mock
from unittest.mock import patch
from django.contrib.auth.models import User
from django.utils.timezone import now
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..managers import SRPManager
from ..models import SrpUserRequest, SrpFleetMain
MODULE_PATH = 'allianceauth.srp.managers'
@@ -13,6 +18,7 @@ currentdir = os.path.dirname(os.path.abspath(inspect.getfile(
inspect.currentframe()
)))
def load_data(filename):
"""loads given JSON file from `testdata` sub folder and returns content"""
with open(
@@ -52,7 +58,7 @@ class TestSrpManager(TestCase):
mock_get.return_value.json.return_value = ['']
with self.assertRaises(ValueError):
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
SRPManager.get_kill_data(81973979)
@patch(MODULE_PATH + '.provider')
@patch(MODULE_PATH + '.requests.get')
@@ -67,6 +73,34 @@ class TestSrpManager(TestCase):
result.return_value = None
with self.assertRaises(ValueError):
ship_type, ship_value, victim_id = SRPManager.get_kill_data(81973979)
SRPManager.get_kill_data(81973979)
def test_pending_requests_count_for_user(self):
user = AuthUtils.create_member("Bruce Wayne")
# when no permission to approve SRP requests
# then return None
self.assertIsNone(SRPManager.pending_requests_count_for_user(user))
# given permission to approve SRP requests
# when no open requests
# then return 0
AuthUtils.add_permission_to_user_by_name("auth.srp_management", user)
user = User.objects.get(pk=user.pk)
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 0)
# given permission to approve SRP requests
# when 1 pending request
# then return 1
fleet = SrpFleetMain.objects.create(fleet_time=now())
SrpUserRequest.objects.create(
killboard_link="https://zkillboard.com/kill/79111612/",
srp_status="Pending",
srp_fleet_main=fleet,
)
SrpUserRequest.objects.create(
killboard_link="https://zkillboard.com/kill/79111612/",
srp_status="Approved",
srp_fleet_main=fleet,
)
self.assertEqual(SRPManager.pending_requests_count_for_user(user), 1)

View File

@@ -1,10 +1,10 @@
// Import the fonts from CDN
@font-face {
font-family: 'Glyphicons Halflings';
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot');
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot');
src: url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.woff') format('woff'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/fonts/glyphicons-halflings-regular.svg#@{icon-font-svg-id}') format('svg');
}

View File

@@ -2,8 +2,8 @@
// 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`
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/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/gh-pages/darkly/bootswatch.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/darkly/variables.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/darkly/bootswatch.less";
@import "../bootstrap-locals.less";
@import "../flatly-shared.less";

File diff suppressed because one or more lines are too long

View File

@@ -2,9 +2,9 @@
// 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`
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/gh-pages/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/gh-pages/flatly/bootswatch.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/bower_components/bootstrap/less/bootstrap.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/flatly/variables.less";
@import "https://raw.githubusercontent.com/thomaspark/bootswatch/v3/flatly/bootswatch.less";
@import "../bootstrap-locals.less";
@import "../flatly-shared.less";

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,6 @@
{% load i18n %}
{% load navactive %}
{% load menu_items %}
{% load groupmanagement %}
<div class="col-sm-2 auth-side-navbar" role="navigation">
<div class="collapse navbar-collapse auth-menus-collapse auth-side-navbar-collapse">
@@ -14,18 +13,9 @@
</li>
<li>
<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>
</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 %}
</li>
{% menu_items %}

View File

@@ -4,17 +4,17 @@
{% if debug %}
<!-- In template debug, loading less file instead of CSS -->
<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 %}
<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 %}
{% else %}
{% if debug %}
<!-- In template debug, loading less file instead of CSS -->
<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 %}
<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 %}
<!-- End Bootstrap CSS -->

View File

@@ -1,6 +1,4 @@
{% load static %}
<!-- Start Bootstrap + jQuery js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- End Bootstrap + jQuery js -->
<!-- Start Bootstrap + jQuery js from cdnjs -->
<script type="application/javascript" 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 -->

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

View File

@@ -1,3 +1,3 @@
<!-- Start DataTables-css -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.15/css/dataTables.bootstrap.min.css"/>
<!-- End DataTables-css -->
<!-- Start Datatables-css from cdnjs -->
<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 from cdnjs -->

View File

@@ -1,4 +1,4 @@
<!-- Start DataTables-js -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.15/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>
<!-- End DataTables-js -->
<!-- Start Datatables-js from cdnjs -->
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.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 from cdnjs -->

View File

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

View File

@@ -1,4 +1,3 @@
{% load static %}
<!-- Start jQuery datetimepicker 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 -->
<!-- Start jQuery-DateTimePicker CSS from cdnjs -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css"/>
<!-- End jQuery-DateTimePicker CSS from cdnjs -->

View File

@@ -1,4 +1,3 @@
{% load static %}
<!-- Start jQuery datetimepicker js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.3.7/jquery.datetimepicker.min.js"></script>
<!-- End jQuery datetimepicker js -->
<!-- Start jQuery-DateTimePicker JS from cdnjs -->
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script>
<!-- End jQuery-DateTimePicker JS from cdnjs -->

View File

@@ -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' %}
<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 %}
<!-- End Moment JS from cdnjs -->

View File

@@ -1,4 +1,3 @@
{% load static %}
<!-- Start X-Editablle js -->
<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 -->
<!-- Start X-editable JS from cdnjs -->
<script type="application/javascript" 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 -->

View File

@@ -1,4 +1,3 @@
{% load staticfiles %}
<!-- X-Editable Core CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css" rel="stylesheet">
<!-- End Bootstrap CSS -->
<!-- Start X-editable CSS from cdnjs -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css"/>
<!-- End X-editable CSS from cdnjs -->

View File

@@ -525,7 +525,7 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="text/javascript">
<script type="application/javascript">
var locale = "{{ LANGUAGE_CODE }}";
var timers = [

View File

@@ -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:
@hooks.register('menu_item_hook')
def register_menu():
return MenuItemHook('Example Item', 'glyphicon glyphicon-heart', 'example_url_name', 150)
```Python
@hooks.register('menu_item_hook')
def register_menu():
return MenuItemHook('Example Item', 'glyphicon glyphicon-heart', 'example_url_name',150)
```
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
#### url_name
### url_name
The name of the Django URL to use
#### order
An integer which specifies the order of the menu item, lowest to highest
#### navactive
### order
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`.
### 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.

View File

@@ -37,11 +37,11 @@ CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
### 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 youll 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`
@@ -52,7 +52,7 @@ Update your auth project's settings file, inputting the server ID as `DISCORD_GU
### 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.
@@ -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.
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.

View File

@@ -216,6 +216,48 @@ On a freshly installed mumble server only your superuser has the right to config
- user: `SuperUser`
- password: *what you defined when configuring your mumble server*
## 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
### Setting a server password
@@ -238,6 +280,7 @@ Save the file and restart your mumble server afterwards.
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
- Use [Mumble temporary links](https://github.com/pvyParts/allianceauth-mumble-temp)
- Use [Mumble temporary links](https://github.com/pvyParts/allianceauth-mumble-temp)

View File

@@ -31,6 +31,7 @@ install_requires = [
'openfire-restapi',
'sleekxmpp',
'pydiscourse',
'django-esi>=1.5,<3.0'
]