mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7412675bfb | ||
|
|
ad953efe77 | ||
|
|
5b26757662 | ||
|
|
8486b95917 | ||
|
|
bb15de6d1a | ||
|
|
0f0c0441a9 | ||
|
|
a0db8e8e2c | ||
|
|
a7f468efd1 | ||
|
|
9f4ab9540b | ||
|
|
1e133b7c5d | ||
|
|
4aa7530bbc | ||
|
|
2e0ddf2e7a | ||
|
|
e24bc2a05d | ||
|
|
a8c0db3fd7 | ||
|
|
7b77a6cd40 | ||
|
|
b8b8e470f2 | ||
|
|
ad92ea243d | ||
|
|
489a8456f7 | ||
|
|
122e389c38 | ||
|
|
8318add6d5 | ||
|
|
6c3650d9f2 | ||
|
|
37005b1c68 | ||
|
|
0897383e41 | ||
|
|
15db817382 | ||
|
|
eaa1cde01a | ||
|
|
7c1d1074f9 | ||
|
|
0f0f9b6062 | ||
|
|
6f2807cba2 | ||
|
|
39a40a8c43 | ||
|
|
5f98b5350e | ||
|
|
9de4d557e3 | ||
|
|
1d5f2634f1 | ||
|
|
710d26d36d | ||
|
|
47793e6198 | ||
|
|
5fcb56a087 | ||
|
|
808080d5ee | ||
|
|
e6037f1680 | ||
|
|
5c3ded6b07 | ||
|
|
0c14e35d15 | ||
|
|
c13be5d39a | ||
|
|
e4b515c1d5 | ||
|
|
65e2c87e8f | ||
|
|
68ce25854a | ||
|
|
3df6643513 | ||
|
|
6ea0ebc9f9 | ||
|
|
26236f5886 | ||
|
|
1420c71ec5 | ||
|
|
5a2c9243c4 | ||
|
|
fecd748198 | ||
|
|
85351b2c66 | ||
|
|
8b3e5b6462 | ||
|
|
93af920b8f | ||
|
|
b1248d9845 | ||
|
|
c86abef07d | ||
|
|
17ad5dfe33 | ||
|
|
e1843fe1f1 | ||
|
|
256a21f058 | ||
|
|
1473259e41 | ||
|
|
e935b91e93 | ||
|
|
07258a6914 | ||
|
|
cb35808508 | ||
|
|
937d656227 | ||
|
|
75a3adb2c9 | ||
|
|
148e208476 | ||
|
|
0036e8b280 | ||
|
|
ea2b5bfecf | ||
|
|
aa7495fa60 | ||
|
|
162ec1bd86 | ||
|
|
2668884008 | ||
|
|
abdc3f3485 | ||
|
|
911f21ee7c | ||
|
|
2387c40254 | ||
|
|
76e18a79b3 | ||
|
|
9725c9c947 | ||
|
|
247ed7cc64 | ||
|
|
2fd2af793e | ||
|
|
3fc36b9ce1 | ||
|
|
c12fd2d7bc | ||
|
|
7fe1ba2fb2 | ||
|
|
ab7eb3e5df |
@@ -1,43 +1,83 @@
|
||||
stages:
|
||||
- gitlab
|
||||
- test
|
||||
- deploy
|
||||
|
||||
include:
|
||||
- template: Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
|
||||
before_script:
|
||||
- apt-get update && apt-get install redis-server -y
|
||||
- redis-server --daemonize yes
|
||||
- redis-cli ping
|
||||
- python -V
|
||||
- pip install wheel tox
|
||||
- apt-get update && apt-get install redis-server -y
|
||||
- redis-server --daemonize yes
|
||||
- python -V
|
||||
- pip install wheel tox
|
||||
|
||||
sast:
|
||||
stage: gitlab
|
||||
before_script: []
|
||||
|
||||
dependency_scanning:
|
||||
stage: gitlab
|
||||
before_script:
|
||||
- apt-get update && apt-get install redis-server libmariadbclient-dev -y
|
||||
- redis-server --daemonize yes
|
||||
- python -V
|
||||
- pip install wheel tox
|
||||
|
||||
test-3.6-core:
|
||||
image: python:3.6-buster
|
||||
script:
|
||||
- tox -e py36-core
|
||||
- tox -e py36-core
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.7-core:
|
||||
image: python:3.7-buster
|
||||
script:
|
||||
- tox -e py37-core
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.8-core:
|
||||
image: python:3.8-buster
|
||||
script:
|
||||
- tox -e py38-core
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.6-all:
|
||||
image: python:3.6-buster
|
||||
script:
|
||||
- tox -e py36-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.7-all:
|
||||
image: python:3.7-buster
|
||||
script:
|
||||
- tox -e py37-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
test-3.8-all:
|
||||
image: python:3.8-buster
|
||||
script:
|
||||
- tox -e py38-all
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
cobertura: coverage.xml
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
@@ -51,4 +91,4 @@ deploy_production:
|
||||
- twine upload dist/*
|
||||
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
- if: $CI_COMMIT_TAG
|
||||
@@ -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.8.2'
|
||||
__version__ = '2.8.8'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = '%s v%s' % (__title__, __version__)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from django.conf import settings
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.models import User as BaseUser, \
|
||||
Permission as BasePermission, Group
|
||||
from django.db.models import Q, F
|
||||
from django.db.models import Count, Q
|
||||
from allianceauth.services.hooks import ServicesHook
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, \
|
||||
post_delete, m2m_changed
|
||||
@@ -24,11 +22,6 @@ from allianceauth.eveonline.tasks import update_character
|
||||
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
|
||||
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
else:
|
||||
_has_auto_groups = False
|
||||
|
||||
|
||||
def make_service_hooks_update_groups_action(service):
|
||||
"""
|
||||
@@ -91,8 +84,7 @@ class UserProfileInline(admin.StackedInline):
|
||||
if request.user.is_superuser:
|
||||
query |= Q(userprofile__isnull=True)
|
||||
else:
|
||||
query |= Q(character_ownership__user=obj)
|
||||
qs = EveCharacter.objects.filter(query)
|
||||
query |= Q(character_ownership__user=obj)
|
||||
formset = super().get_formset(request, obj=obj, **kwargs)
|
||||
|
||||
def get_kwargs(self, index):
|
||||
@@ -121,6 +113,8 @@ def user_profile_pic(obj):
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
user_profile_pic.short_description = ''
|
||||
|
||||
|
||||
@@ -152,6 +146,7 @@ def user_username(obj):
|
||||
user_obj.username,
|
||||
)
|
||||
|
||||
|
||||
user_username.short_description = 'user / main'
|
||||
user_username.admin_order_field = 'username'
|
||||
|
||||
@@ -168,7 +163,8 @@ def user_main_organization(obj):
|
||||
else:
|
||||
corporation = user_obj.profile.main_character.corporation_name
|
||||
if user_obj.profile.main_character.alliance_id:
|
||||
result = format_html('{}<br>{}',
|
||||
result = format_html(
|
||||
'{}<br>{}',
|
||||
corporation,
|
||||
user_obj.profile.main_character.alliance_name
|
||||
)
|
||||
@@ -176,6 +172,7 @@ def user_main_organization(obj):
|
||||
result = corporation
|
||||
return result
|
||||
|
||||
|
||||
user_main_organization.short_description = 'Corporation / Alliance (Main)'
|
||||
user_main_organization.admin_order_field = \
|
||||
'profile__main_character__corporation_name'
|
||||
@@ -205,13 +202,13 @@ class MainCorporationsFilter(admin.SimpleListFilter):
|
||||
return qs.all()
|
||||
else:
|
||||
if qs.model == User:
|
||||
return qs\
|
||||
.filter(profile__main_character__corporation_id=\
|
||||
self.value())
|
||||
return qs.filter(
|
||||
profile__main_character__corporation_id=self.value()
|
||||
)
|
||||
else:
|
||||
return qs\
|
||||
.filter(user__profile__main_character__corporation_id=\
|
||||
self.value())
|
||||
return qs.filter(
|
||||
user__profile__main_character__corporation_id=self.value()
|
||||
)
|
||||
|
||||
|
||||
class MainAllianceFilter(admin.SimpleListFilter):
|
||||
@@ -239,12 +236,11 @@ class MainAllianceFilter(admin.SimpleListFilter):
|
||||
return qs.all()
|
||||
else:
|
||||
if qs.model == User:
|
||||
return qs\
|
||||
.filter(profile__main_character__alliance_id=self.value())
|
||||
return qs.filter(profile__main_character__alliance_id=self.value())
|
||||
else:
|
||||
return qs\
|
||||
.filter(user__profile__main_character__alliance_id=\
|
||||
self.value())
|
||||
return qs.filter(
|
||||
user__profile__main_character__alliance_id=self.value()
|
||||
)
|
||||
|
||||
|
||||
def update_main_character_model(modeladmin, request, queryset):
|
||||
@@ -259,6 +255,7 @@ def update_main_character_model(modeladmin, request, queryset):
|
||||
'Update from ESI started for {} characters'.format(tasks_count)
|
||||
)
|
||||
|
||||
|
||||
update_main_character_model.short_description = \
|
||||
'Update main character model from ESI'
|
||||
|
||||
@@ -267,32 +264,16 @@ class UserAdmin(BaseUserAdmin):
|
||||
"""Extending Django's UserAdmin model
|
||||
|
||||
Behavior of groups and characters columns can be configured via settings
|
||||
|
||||
"""
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
"all": ("authentication/css/admin.css",)
|
||||
}
|
||||
|
||||
class RealGroupsFilter(admin.SimpleListFilter):
|
||||
"""Custom filter to get groups w/o Autogroups"""
|
||||
title = 'group'
|
||||
parameter_name = 'group_id__exact'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
qs = Group.objects.all().order_by(Lower('name'))
|
||||
if _has_auto_groups:
|
||||
qs = qs\
|
||||
.filter(managedalliancegroup__isnull=True)\
|
||||
.filter(managedcorpgroup__isnull=True)
|
||||
return tuple([(x.pk, x.name) for x in qs])
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is None:
|
||||
return queryset.all()
|
||||
else:
|
||||
return queryset.filter(groups__pk=self.value())
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
return qs.prefetch_related("character_ownerships__character", "groups")
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(BaseUserAdmin, self).get_actions(request)
|
||||
@@ -341,11 +322,9 @@ class UserAdmin(BaseUserAdmin):
|
||||
return result
|
||||
|
||||
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
||||
|
||||
ordering = ('username', )
|
||||
list_select_related = True
|
||||
show_full_result_count = True
|
||||
|
||||
ordering = ('username', )
|
||||
list_select_related = ('profile__state', 'profile__main_character')
|
||||
show_full_result_count = True
|
||||
list_display = (
|
||||
user_profile_pic,
|
||||
user_username,
|
||||
@@ -358,10 +337,9 @@ class UserAdmin(BaseUserAdmin):
|
||||
'_role'
|
||||
)
|
||||
list_display_links = None
|
||||
|
||||
list_filter = (
|
||||
'profile__state',
|
||||
RealGroupsFilter,
|
||||
'groups',
|
||||
MainCorporationsFilter,
|
||||
MainAllianceFilter,
|
||||
'is_active',
|
||||
@@ -375,41 +353,25 @@ class UserAdmin(BaseUserAdmin):
|
||||
)
|
||||
|
||||
def _characters(self, obj):
|
||||
my_characters = [
|
||||
x.character.character_name
|
||||
for x in CharacterOwnership.objects\
|
||||
.filter(user=obj)\
|
||||
.order_by('character__character_name')\
|
||||
.select_related()
|
||||
]
|
||||
character_ownerships = list(obj.character_ownerships.all())
|
||||
characters = [obj.character.character_name for obj in character_ownerships]
|
||||
return self._list_2_html_w_tooltips(
|
||||
my_characters,
|
||||
sorted(characters),
|
||||
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
||||
)
|
||||
|
||||
_characters.short_description = 'characters'
|
||||
|
||||
|
||||
def _state(self, obj):
|
||||
return obj.profile.state.name
|
||||
|
||||
_state.short_description = 'state'
|
||||
_state.admin_order_field = 'profile__state'
|
||||
|
||||
def _groups(self, obj):
|
||||
if not _has_auto_groups:
|
||||
my_groups = [x.name for x in obj.groups.order_by('name')]
|
||||
else:
|
||||
my_groups = [
|
||||
x.name for x in obj.groups\
|
||||
.filter(managedalliancegroup__isnull=True)\
|
||||
.filter(managedcorpgroup__isnull=True)\
|
||||
.order_by('name')
|
||||
]
|
||||
|
||||
def _groups(self, obj):
|
||||
my_groups = sorted([group.name for group in list(obj.groups.all())])
|
||||
return self._list_2_html_w_tooltips(
|
||||
my_groups,
|
||||
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
||||
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
||||
)
|
||||
|
||||
_groups.short_description = 'groups'
|
||||
@@ -437,7 +399,7 @@ class UserAdmin(BaseUserAdmin):
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
"""overriding this formfield to have sorted lists in the form"""
|
||||
if db_field.name == "groups":
|
||||
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
|
||||
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
|
||||
@@ -446,9 +408,14 @@ class StateAdmin(admin.ModelAdmin):
|
||||
list_select_related = True
|
||||
list_display = ('name', 'priority', '_user_count')
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
return qs.annotate(user_count=Count("userprofile__id"))
|
||||
|
||||
def _user_count(self, obj):
|
||||
return obj.userprofile_set.all().count()
|
||||
return obj.user_count
|
||||
_user_count.short_description = 'Users'
|
||||
_user_count.admin_order_field = 'user_count'
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -471,7 +438,7 @@ class StateAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
"""overriding this formfield to have sorted lists in the form"""
|
||||
"""overriding this formfield to have sorted lists in the form"""
|
||||
if db_field.name == "member_characters":
|
||||
kwargs["queryset"] = EveCharacter.objects.all()\
|
||||
.order_by(Lower('character_name'))
|
||||
@@ -481,8 +448,10 @@ class StateAdmin(admin.ModelAdmin):
|
||||
elif db_field.name == "member_alliances":
|
||||
kwargs["queryset"] = EveAllianceInfo.objects.all()\
|
||||
.order_by(Lower('alliance_name'))
|
||||
elif db_field.name == "permissions":
|
||||
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
if obj == get_guest_state():
|
||||
return False
|
||||
@@ -504,7 +473,8 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
|
||||
"all": ("authentication/css/admin.css",)
|
||||
}
|
||||
|
||||
list_select_related = True
|
||||
list_select_related = (
|
||||
'user__profile__state', 'user__profile__main_character', 'character')
|
||||
list_display = (
|
||||
user_profile_pic,
|
||||
user_username,
|
||||
@@ -542,6 +512,7 @@ class CharacterOwnershipAdmin(BaseOwnershipAdmin):
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
readonly_fields = [field.name for field in BasePermission._meta.fields]
|
||||
search_fields = ('codename', )
|
||||
list_display = ('admin_name', 'name', 'codename', 'content_type')
|
||||
list_filter = ('content_type__app_label',)
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ def get_admin_change_view_url(obj: object) -> str:
|
||||
args=(obj.pk,)
|
||||
)
|
||||
|
||||
|
||||
def get_admin_search_url(ModelClass: type) -> str:
|
||||
"""returns URL to search URL for model of given object"""
|
||||
return '{}{}/'.format(
|
||||
reverse('admin:app_list', args=(ModelClass._meta.app_label,)),
|
||||
ModelClass.__name__.lower()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from urllib.parse import quote
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth.models import User as BaseUser, Group
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
|
||||
from allianceauth.authentication.models import (
|
||||
@@ -18,8 +16,7 @@ from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..admin import (
|
||||
BaseUserAdmin,
|
||||
CharacterOwnershipAdmin,
|
||||
PermissionAdmin,
|
||||
CharacterOwnershipAdmin,
|
||||
StateAdmin,
|
||||
MainCorporationsFilter,
|
||||
MainAllianceFilter,
|
||||
@@ -35,11 +32,6 @@ from ..admin import (
|
||||
)
|
||||
from . import get_admin_change_view_url, get_admin_search_url
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
|
||||
else:
|
||||
_has_auto_groups = False
|
||||
|
||||
MODULE_PATH = 'allianceauth.authentication.admin'
|
||||
|
||||
@@ -48,6 +40,7 @@ class MockRequest(object):
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
|
||||
class TestCaseWithTestData(TestCase):
|
||||
|
||||
@classmethod
|
||||
@@ -279,6 +272,7 @@ class TestStateAdmin(TestCaseWithTestData):
|
||||
expected = 200
|
||||
self.assertEqual(response.status_code, expected)
|
||||
|
||||
|
||||
class TestUserAdmin(TestCaseWithTestData):
|
||||
|
||||
def setUp(self):
|
||||
@@ -287,24 +281,12 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
model=User, admin_site=AdminSite()
|
||||
)
|
||||
self.character_1 = self.user_1.character_ownerships.first().character
|
||||
|
||||
def _create_autogroups(self):
|
||||
"""create autogroups for corps and alliances"""
|
||||
if _has_auto_groups:
|
||||
autogroups_config = AutogroupsConfig(
|
||||
corp_groups = True,
|
||||
alliance_groups = True
|
||||
)
|
||||
autogroups_config.save()
|
||||
for state in State.objects.all():
|
||||
autogroups_config.states.add(state)
|
||||
autogroups_config.update_corp_group_membership(self.user_1)
|
||||
|
||||
# column rendering
|
||||
|
||||
|
||||
def test_user_profile_pic_u1(self):
|
||||
expected = ('<img src="https://images.evetech.net/characters/1001/'
|
||||
'portrait?size=32" class="img-circle">')
|
||||
expected = (
|
||||
'<img src="https://images.evetech.net/characters/1001/'
|
||||
'portrait?size=32" class="img-circle">'
|
||||
)
|
||||
self.assertEqual(user_profile_pic(self.user_1), expected)
|
||||
|
||||
def test_user_profile_pic_u3(self):
|
||||
@@ -351,37 +333,17 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
result = self.modeladmin._characters(self.user_3)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_groups_u1(self):
|
||||
self._create_autogroups()
|
||||
def test_groups_u1(self):
|
||||
expected = 'Group 1'
|
||||
result = self.modeladmin._groups(self.user_1)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_groups_u2(self):
|
||||
self._create_autogroups()
|
||||
def test_groups_u2(self):
|
||||
expected = 'Group 2'
|
||||
result = self.modeladmin._groups(self.user_2)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_groups_u3(self):
|
||||
self._create_autogroups()
|
||||
result = self.modeladmin._groups(self.user_3)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch(MODULE_PATH + '._has_auto_groups', False)
|
||||
def test_groups_u1_no_autogroups(self):
|
||||
expected = 'Group 1'
|
||||
result = self.modeladmin._groups(self.user_1)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@patch(MODULE_PATH + '._has_auto_groups', False)
|
||||
def test_groups_u2_no_autogroups(self):
|
||||
expected = 'Group 2'
|
||||
result = self.modeladmin._groups(self.user_2)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@patch(MODULE_PATH + '._has_auto_groups', False)
|
||||
def test_groups_u3_no_autogroups(self):
|
||||
def test_groups_u3(self):
|
||||
result = self.modeladmin._groups(self.user_3)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@@ -413,8 +375,10 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
|
||||
def test_list_2_html_w_tooltips_w_cutoff(self):
|
||||
items = ['one', 'two', 'three']
|
||||
expected = ('<span data-tooltip="one, two, three" '
|
||||
'class="tooltip">one, two, (...)</span>')
|
||||
expected = (
|
||||
'<span data-tooltip="one, two, three" '
|
||||
'class="tooltip">one, two, (...)</span>'
|
||||
)
|
||||
result = self.modeladmin._list_2_html_w_tooltips(items, 2)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@@ -439,63 +403,7 @@ class TestUserAdmin(TestCaseWithTestData):
|
||||
self.assertTrue(mock_message_user.called)
|
||||
|
||||
# filters
|
||||
|
||||
def test_filter_real_groups_with_autogroups(self):
|
||||
|
||||
class UserAdminTest(BaseUserAdmin):
|
||||
list_filter = (UserAdmin.RealGroupsFilter,)
|
||||
|
||||
self._create_autogroups()
|
||||
my_modeladmin = UserAdminTest(User, AdminSite())
|
||||
|
||||
# Make sure the lookups are correct
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user_1
|
||||
changelist = my_modeladmin.get_changelist_instance(request)
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
(self.group_1.pk, self.group_1.name),
|
||||
(self.group_2.pk, self.group_2.name),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
# Make sure the correct queryset is returned
|
||||
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
|
||||
request.user = self.user_1
|
||||
changelist = my_modeladmin.get_changelist_instance(request)
|
||||
queryset = changelist.get_queryset(request)
|
||||
expected = User.objects.filter(groups__in=[self.group_1])
|
||||
self.assertSetEqual(set(queryset), set(expected))
|
||||
|
||||
@patch(MODULE_PATH + '._has_auto_groups', False)
|
||||
def test_filter_real_groups_no_autogroups(self):
|
||||
|
||||
class UserAdminTest(BaseUserAdmin):
|
||||
list_filter = (UserAdmin.RealGroupsFilter,)
|
||||
|
||||
my_modeladmin = UserAdminTest(User, AdminSite())
|
||||
|
||||
# Make sure the lookups are correct
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user_1
|
||||
changelist = my_modeladmin.get_changelist_instance(request)
|
||||
filters = changelist.get_filters(request)
|
||||
filterspec = filters[0][0]
|
||||
expected = [
|
||||
(self.group_1.pk, self.group_1.name),
|
||||
(self.group_2.pk, self.group_2.name),
|
||||
]
|
||||
self.assertEqual(filterspec.lookup_choices, expected)
|
||||
|
||||
# Make sure the correct queryset is returned
|
||||
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
|
||||
request.user = self.user_1
|
||||
changelist = my_modeladmin.get_changelist_instance(request)
|
||||
queryset = changelist.get_queryset(request)
|
||||
expected = User.objects.filter(groups__in=[self.group_1])
|
||||
self.assertSetEqual(set(queryset), set(expected))
|
||||
|
||||
|
||||
def test_filter_main_corporations(self):
|
||||
|
||||
class UserAdminTest(BaseUserAdmin):
|
||||
@@ -603,7 +511,6 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
|
||||
def sync_nicknames_bulk(self, user):
|
||||
pass
|
||||
|
||||
|
||||
def test_service_has_update_groups_only(self):
|
||||
service = self.MyServicesHookTypeA()
|
||||
mock_service = MagicMock(spec=service)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from math import ceil
|
||||
from unittest.mock import patch
|
||||
|
||||
from requests import RequestException
|
||||
import requests_mock
|
||||
from packaging.version import Version as Pep440Version
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.templatetags.admin_status import (
|
||||
@@ -12,8 +12,7 @@ from allianceauth.templatetags.admin_status import (
|
||||
_fetch_list_from_gitlab,
|
||||
_current_notifications,
|
||||
_current_version_summary,
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
_fetch_tags_from_gitlab,
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
_latests_versions
|
||||
)
|
||||
|
||||
@@ -103,35 +102,51 @@ class TestStatusOverviewTag(TestCase):
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
cache.clear()
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
|
||||
# given
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
|
||||
'?labels=announcement'
|
||||
)
|
||||
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
|
||||
# when
|
||||
result = _fetch_notification_issues_from_gitlab()
|
||||
# then
|
||||
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_notifications_normal(self, mock_cache):
|
||||
# given
|
||||
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
|
||||
|
||||
# when
|
||||
result = _current_notifications()
|
||||
# then
|
||||
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_notifications_failed(self, mock_cache):
|
||||
mock_cache.get_or_set.side_effect = RequestException
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_current_notifications_failed(self, requests_mocker):
|
||||
# given
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
|
||||
'?labels=announcement'
|
||||
)
|
||||
requests_mocker.get(url, status_code=404)
|
||||
# when
|
||||
result = _current_notifications()
|
||||
# then
|
||||
self.assertEqual(result['notifications'], list())
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_notifications_is_none(self, mock_cache):
|
||||
# given
|
||||
mock_cache.get_or_set.return_value = None
|
||||
|
||||
# when
|
||||
result = _current_notifications()
|
||||
# then
|
||||
self.assertEqual(result['notifications'], list())
|
||||
|
||||
|
||||
@@ -143,12 +158,17 @@ class TestCeleryQueueLength(TestCase):
|
||||
|
||||
class TestVersionTags(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
cache.clear()
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_version_info_normal(self, mock_cache):
|
||||
def test_current_version_info_normal(self, mock_cache):
|
||||
# given
|
||||
mock_cache.get_or_set.return_value = GITHUB_TAGS
|
||||
|
||||
# when
|
||||
result = _current_version_summary()
|
||||
# then
|
||||
self.assertTrue(result['latest_major'])
|
||||
self.assertTrue(result['latest_minor'])
|
||||
self.assertTrue(result['latest_patch'])
|
||||
@@ -158,32 +178,41 @@ class TestVersionTags(TestCase):
|
||||
self.assertEqual(result['latest_beta_version'], '2.4.6a1')
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_version_info_failed(self, mock_cache):
|
||||
mock_cache.get_or_set.side_effect = RequestException
|
||||
|
||||
expected = {}
|
||||
@requests_mock.mock()
|
||||
def test_current_version_info_failed(self, requests_mocker):
|
||||
# given
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||
'/repository/tags'
|
||||
)
|
||||
requests_mocker.get(url, status_code=500)
|
||||
# when
|
||||
result = _current_version_summary()
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, {})
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_fetch_tags_from_gitlab(self, requests_mocker):
|
||||
# given
|
||||
url = (
|
||||
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||
'/repository/tags'
|
||||
)
|
||||
requests_mocker.get(url, json=GITHUB_TAGS)
|
||||
result = _fetch_tags_from_gitlab()
|
||||
self.assertEqual(result, GITHUB_TAGS)
|
||||
# when
|
||||
result = _current_version_summary()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status.cache')
|
||||
def test_current_version_info_return_no_data(self, mock_cache):
|
||||
mock_cache.get_or_set.return_value = None
|
||||
|
||||
expected = {}
|
||||
# given
|
||||
mock_cache.get_or_set.return_value = None
|
||||
# when
|
||||
result = _current_version_summary()
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, {})
|
||||
|
||||
|
||||
class TestLatestsVersion(TestCase):
|
||||
|
||||
@@ -96,24 +96,62 @@ class EveAllianceForm(EveEntityForm):
|
||||
|
||||
@admin.register(EveCorporationInfo)
|
||||
class EveCorporationInfoAdmin(admin.ModelAdmin):
|
||||
search_fields = ['corporation_name']
|
||||
list_display = ('corporation_name', 'alliance')
|
||||
list_select_related = ('alliance',)
|
||||
list_filter = (('alliance', admin.RelatedOnlyFieldListFilter),)
|
||||
ordering = ('corporation_name',)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
if not obj or not obj.pk:
|
||||
return EveCorporationForm
|
||||
return super(EveCorporationInfoAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
return super().get_form(request, obj=obj, **kwargs)
|
||||
|
||||
|
||||
@admin.register(EveAllianceInfo)
|
||||
class EveAllianceInfoAdmin(admin.ModelAdmin):
|
||||
search_fields = ['alliance_name']
|
||||
list_display = ('alliance_name',)
|
||||
ordering = ('alliance_name',)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
if not obj or not obj.pk:
|
||||
return EveAllianceForm
|
||||
return super(EveAllianceInfoAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
return super().get_form(request, obj=obj, **kwargs)
|
||||
|
||||
|
||||
@admin.register(EveCharacter)
|
||||
class EveCharacterAdmin(admin.ModelAdmin):
|
||||
search_fields = ['character_name', 'corporation_name', 'alliance_name', 'character_ownership__user__username']
|
||||
list_display = ('character_name', 'corporation_name', 'alliance_name', 'user', 'main_character')
|
||||
search_fields = [
|
||||
'character_name',
|
||||
'corporation_name',
|
||||
'alliance_name',
|
||||
'character_ownership__user__username'
|
||||
]
|
||||
list_display = (
|
||||
'character_name', 'corporation_name', 'alliance_name', 'user', 'main_character'
|
||||
)
|
||||
list_select_related = (
|
||||
'character_ownership', 'character_ownership__user__profile__main_character'
|
||||
)
|
||||
list_filter = (
|
||||
'corporation_name',
|
||||
'alliance_name',
|
||||
(
|
||||
'character_ownership__user__profile__main_character',
|
||||
admin.RelatedOnlyFieldListFilter
|
||||
),
|
||||
)
|
||||
ordering = ('character_name', )
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def user(obj):
|
||||
|
||||
@@ -32,10 +32,12 @@ class EveCharacterManager(models.Manager):
|
||||
def update_character(self, character_id):
|
||||
return self.get(character_id=character_id).update_character()
|
||||
|
||||
def get_character_by_id(self, char_id):
|
||||
if self.filter(character_id=char_id).exists():
|
||||
return self.get(character_id=char_id)
|
||||
return None
|
||||
def get_character_by_id(self, character_id: int):
|
||||
"""Return character by character ID or None if not found."""
|
||||
try:
|
||||
return self.get(character_id=character_id)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class EveAllianceProviderManager:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.conf import settings
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group as BaseGroup, User
|
||||
from django.db.models import Count
|
||||
@@ -10,9 +10,8 @@ from django.dispatch import receiver
|
||||
|
||||
from .models import AuthGroup
|
||||
from .models import GroupRequest
|
||||
from . import signals
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
if 'eve_autogroups' in apps.app_configs:
|
||||
_has_auto_groups = True
|
||||
else:
|
||||
_has_auto_groups = False
|
||||
@@ -41,9 +40,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
|
||||
kwargs["queryset"] = Group.objects.order_by(Lower('name'))
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -97,9 +93,9 @@ class HasLeaderFilter(admin.SimpleListFilter):
|
||||
else:
|
||||
return queryset
|
||||
|
||||
class GroupAdmin(admin.ModelAdmin):
|
||||
list_select_related = True
|
||||
ordering = ('name', )
|
||||
|
||||
class GroupAdmin(admin.ModelAdmin):
|
||||
ordering = ('name',)
|
||||
list_display = (
|
||||
'name',
|
||||
'_description',
|
||||
@@ -118,9 +114,12 @@ class GroupAdmin(admin.ModelAdmin):
|
||||
list_filter.append(HasLeaderFilter)
|
||||
|
||||
search_fields = ('name', 'authgroup__description')
|
||||
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
if _has_auto_groups:
|
||||
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
|
||||
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
|
||||
qs = qs.annotate(
|
||||
member_count=Count('user', distinct=True),
|
||||
)
|
||||
@@ -136,7 +135,7 @@ class GroupAdmin(admin.ModelAdmin):
|
||||
_member_count.admin_order_field = 'member_count'
|
||||
|
||||
def has_leader(self, obj):
|
||||
return obj.authgroup.group_leaders.exists()
|
||||
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
|
||||
|
||||
has_leader.boolean = True
|
||||
|
||||
@@ -166,6 +165,18 @@ class GroupAdmin(admin.ModelAdmin):
|
||||
filter_horizontal = ('permissions',)
|
||||
inlines = (AuthGroupInlineAdmin,)
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
if db_field.name == "permissions":
|
||||
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
for inline_form in formset:
|
||||
ag_instance = inline_form.save(commit=False)
|
||||
ag_instance.group = form.instance
|
||||
ag_instance.save()
|
||||
formset.save()
|
||||
|
||||
|
||||
class Group(BaseGroup):
|
||||
class Meta:
|
||||
@@ -173,13 +184,29 @@ class Group(BaseGroup):
|
||||
verbose_name = BaseGroup._meta.verbose_name
|
||||
verbose_name_plural = BaseGroup._meta.verbose_name_plural
|
||||
|
||||
|
||||
try:
|
||||
admin.site.unregister(BaseGroup)
|
||||
finally:
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
|
||||
|
||||
admin.site.register(GroupRequest)
|
||||
@admin.register(GroupRequest)
|
||||
class GroupRequestAdmin(admin.ModelAdmin):
|
||||
search_fields = ('user__username', )
|
||||
list_display = ('id', 'group', 'user', '_leave_request', 'status')
|
||||
list_filter = (
|
||||
('group', admin.RelatedOnlyFieldListFilter),
|
||||
('user', admin.RelatedOnlyFieldListFilter),
|
||||
'leave_request',
|
||||
'status'
|
||||
)
|
||||
|
||||
def _leave_request(self, obj) -> True:
|
||||
return obj.leave_request
|
||||
|
||||
_leave_request.short_description = 'is leave request'
|
||||
_leave_request.boolean = True
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Group)
|
||||
|
||||
@@ -5,3 +5,6 @@ class GroupManagementConfig(AppConfig):
|
||||
name = 'allianceauth.groupmanagement'
|
||||
label = 'groupmanagement'
|
||||
verbose_name = 'Group Management'
|
||||
|
||||
def ready(self):
|
||||
from . import signals # noqa: F401
|
||||
|
||||
@@ -55,7 +55,6 @@ class RequestLog(models.Model):
|
||||
return user.profile.main_character
|
||||
|
||||
|
||||
|
||||
class AuthGroup(models.Model):
|
||||
"""
|
||||
Extends Django Group model with a one-to-one field
|
||||
@@ -107,7 +106,8 @@ class AuthGroup(models.Model):
|
||||
help_text="States listed here will have the ability to join this group provided "
|
||||
"they have the proper permissions.")
|
||||
|
||||
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)</i> of the group shown to users.")
|
||||
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
|
||||
"</i> of the group shown to users.")
|
||||
|
||||
def __str__(self):
|
||||
return self.group.name
|
||||
|
||||
@@ -40,120 +40,121 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="add" class="tab-pane fade in active panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% if acceptrequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Character" %}</th>
|
||||
<th>{% trans "Organization" %}</th>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for acceptrequest in acceptrequests %}
|
||||
<div class="panel panel-default panel-tabs-aa">
|
||||
<div class="panel-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<div id="add" class="tab-pane active">
|
||||
{% if acceptrequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ acceptrequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% trans "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ acceptrequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
|
||||
{% trans "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
|
||||
{% trans "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
<th>{% trans "Character" %}</th>
|
||||
<th>{% trans "Organization" %}</th>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</thead>
|
||||
|
||||
<div id="leave" class="tab-pane fade panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Character" %}</th>
|
||||
<th>{% trans "Organization" %}</th>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for acceptrequest in acceptrequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ acceptrequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if acceptrequest.main_char %}
|
||||
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ acceptrequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% trans "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ acceptrequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
|
||||
{% trans "Accept" %}
|
||||
</a>
|
||||
|
||||
<tbody>
|
||||
{% for leaverequest in leaverequests %}
|
||||
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
|
||||
{% trans "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="leave" class="tab-pane">
|
||||
{% if leaverequests %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-aa">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% trans "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% trans "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% trans "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
<th>{% trans "Character" %}</th>
|
||||
<th>{% trans "Organization" %}</th>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for leaverequest in leaverequests %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.character_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ leaverequest.user.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if leaverequest.main_char %}
|
||||
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
|
||||
{{ leaverequest.main_char.corporation_name }}
|
||||
</a><br>
|
||||
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
|
||||
{% else %}
|
||||
{% trans "(unknown)" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ leaverequest.group.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
|
||||
{% trans "Accept" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
|
||||
{% trans "Reject" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,8 @@ class TestGroupAdmin(TestCase):
|
||||
# group 2 - no leader
|
||||
cls.group_2 = Group.objects.create(name='Group 2')
|
||||
cls.group_2.authgroup.description = 'Internal Group'
|
||||
cls.group_2.authgroup.internal = True
|
||||
cls.group_2.authgroup.internal = True
|
||||
cls.group_2.authgroup.group_leader_groups.add(cls.group_1)
|
||||
cls.group_2.authgroup.save()
|
||||
|
||||
# group 3 - has leader
|
||||
@@ -237,10 +238,14 @@ class TestGroupAdmin(TestCase):
|
||||
result = self.modeladmin._member_count(obj)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_has_leader(self):
|
||||
def test_has_leader_user(self):
|
||||
result = self.modeladmin.has_leader(self.group_1)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_has_leader_group(self):
|
||||
result = self.modeladmin.has_leader(self.group_2)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_properties_1(self):
|
||||
expected = ['Default']
|
||||
result = self.modeladmin._properties(self.group_1)
|
||||
|
||||
@@ -1,4 +1,35 @@
|
||||
from django.contrib import admin
|
||||
from .models import Notification
|
||||
|
||||
admin.site.register(Notification)
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationAdmin(admin.ModelAdmin):
|
||||
list_display = ("timestamp", "_main", "_state", "title", "level", "viewed")
|
||||
list_select_related = ("user", "user__profile__main_character", "user__profile__state")
|
||||
list_filter = (
|
||||
"level",
|
||||
"timestamp",
|
||||
"user__profile__state",
|
||||
('user__profile__main_character', admin.RelatedOnlyFieldListFilter),
|
||||
)
|
||||
ordering = ("-timestamp", )
|
||||
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
||||
|
||||
def _main(self, obj):
|
||||
try:
|
||||
return obj.user.profile.main_character
|
||||
except AttributeError:
|
||||
return obj.user
|
||||
|
||||
_main.admin_order_field = "user__profile__main_character__character_name"
|
||||
|
||||
def _state(self, obj):
|
||||
return obj.user.profile.state
|
||||
|
||||
_state.admin_order_field = "user__profile__state__name"
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request) -> bool:
|
||||
return False
|
||||
|
||||
@@ -12,21 +12,20 @@ class NotificationHandler(logging.Handler):
|
||||
|
||||
try:
|
||||
perm = Permission.objects.get(codename="logging_notifications")
|
||||
|
||||
message = record.getMessage()
|
||||
if record.exc_text:
|
||||
message += "\n\n"
|
||||
message = message + record.exc_text
|
||||
|
||||
users = User.objects.filter(
|
||||
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
|
||||
|
||||
for user in users:
|
||||
notify(
|
||||
user,
|
||||
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
|
||||
level=str([item[0] for item in Notification.LEVEL_CHOICES if item[1] == record.levelname][0]),
|
||||
message=message
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
return
|
||||
|
||||
message = record.getMessage()
|
||||
if record.exc_text:
|
||||
message += "\n\n"
|
||||
message = message + record.exc_text
|
||||
|
||||
users = User.objects.filter(
|
||||
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
|
||||
for user in users:
|
||||
notify(
|
||||
user,
|
||||
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
|
||||
level=Notification.Level.from_old_name(record.levelname),
|
||||
message=message
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ class NotificationQuerySet(models.QuerySet):
|
||||
"""Custom QuerySet for Notification model"""
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# overriden update to ensure cache is invaidated on very call
|
||||
"""Override update to ensure cache is invalidated on very call."""
|
||||
super().update(*args, **kwargs)
|
||||
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
|
||||
for user_pk in user_pks:
|
||||
@@ -43,6 +43,8 @@ class NotificationManager(models.Manager):
|
||||
if not message:
|
||||
message = title
|
||||
|
||||
if level not in self.model.Level:
|
||||
level = self.model.Level.INFO
|
||||
obj = self.create(user=user, title=title, message=message, level=level)
|
||||
logger.info("Created notification %s", obj)
|
||||
return obj
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.12 on 2021-07-01 21:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0004_performance_tuning'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='level',
|
||||
field=models.CharField(choices=[('danger', 'danger'), ('warning', 'warning'), ('info', 'info'), ('success', 'success')], default='info', max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .managers import NotificationManager
|
||||
|
||||
@@ -13,17 +14,43 @@ class Notification(models.Model):
|
||||
|
||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
|
||||
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
|
||||
|
||||
LEVEL_CHOICES = (
|
||||
('danger', 'CRITICAL'),
|
||||
('danger', 'ERROR'),
|
||||
('warning', 'WARN'),
|
||||
('info', 'INFO'),
|
||||
('success', 'DEBUG'),
|
||||
)
|
||||
|
||||
class Level(models.TextChoices):
|
||||
"""A notification level."""
|
||||
|
||||
DANGER = 'danger', _('danger') #:
|
||||
WARNING = 'warning', _('warning') #:
|
||||
INFO = 'info', _('info') #:
|
||||
SUCCESS = 'success', _('success') #:
|
||||
|
||||
@classmethod
|
||||
def from_old_name(cls, name: str) -> object:
|
||||
"""Map old name to enum.
|
||||
|
||||
Raises ValueError for invalid names.
|
||||
"""
|
||||
name_map = {
|
||||
"CRITICAL": cls.DANGER,
|
||||
"ERROR": cls.DANGER,
|
||||
"WARN": cls.WARNING,
|
||||
"INFO": cls.INFO,
|
||||
"DEBUG": cls.SUCCESS,
|
||||
}
|
||||
try:
|
||||
return name_map[name]
|
||||
except KeyError:
|
||||
raise ValueError(f"Unknown name: {name}") from None
|
||||
|
||||
# LEVEL_CHOICES = (
|
||||
# ('danger', 'CRITICAL'),
|
||||
# ('danger', 'ERROR'),
|
||||
# ('warning', 'WARN'),
|
||||
# ('info', 'INFO'),
|
||||
# ('success', 'DEBUG'),
|
||||
# )
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
level = models.CharField(choices=LEVEL_CHOICES, max_length=10)
|
||||
level = models.CharField(choices=Level.choices, max_length=10, default=Level.INFO)
|
||||
title = models.CharField(max_length=254)
|
||||
message = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
@@ -45,22 +72,15 @@ class Notification(models.Model):
|
||||
Notification.objects.invalidate_user_notification_cache(self.user.pk)
|
||||
|
||||
def mark_viewed(self) -> None:
|
||||
"""mark notification as viewed"""
|
||||
"""Mark notification as viewed."""
|
||||
logger.info("Marking notification as viewed: %s" % self)
|
||||
self.viewed = True
|
||||
self.save()
|
||||
|
||||
def set_level(self, level_name: str) -> None:
|
||||
"""set notification level according to level name, e.g. 'CRITICAL'
|
||||
"""Set notification level according to old level name, e.g. 'CRITICAL'.
|
||||
|
||||
raised exception on invalid level names
|
||||
"""
|
||||
try:
|
||||
new_level = [
|
||||
item[0] for item in self.LEVEL_CHOICES if item[1] == level_name
|
||||
][0]
|
||||
except IndexError:
|
||||
raise ValueError('Invalid level name: %s' % level_name)
|
||||
|
||||
self.level = new_level
|
||||
Raises ValueError on invalid level names.
|
||||
"""
|
||||
self.level = self.Level.from_old_name(level_name)
|
||||
self.save()
|
||||
|
||||
@@ -33,10 +33,8 @@ def user_unread_notification_count(user: object) -> int:
|
||||
|
||||
@register.simple_tag
|
||||
def notifications_refresh_time() -> int:
|
||||
refresh_time = getattr(settings, 'NOTIFICATIONS_REFRESH_TIME', None)
|
||||
if (
|
||||
refresh_time is None or not isinstance(refresh_time, int) or refresh_time < 0
|
||||
):
|
||||
refresh_time = getattr(settings, 'NOTIFICATIONS_REFRESH_TIME', Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT)
|
||||
if (not isinstance(refresh_time, int) or refresh_time < 0):
|
||||
logger.warning('NOTIFICATIONS_REFRESH_TIME setting is invalid. Using default.')
|
||||
refresh_time = Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT
|
||||
|
||||
|
||||
69
allianceauth/notifications/tests/test_handlers.py
Normal file
69
allianceauth/notifications/tests/test_handlers.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from logging import LogRecord, DEBUG
|
||||
|
||||
from django.contrib.auth.models import Permission, Group, User
|
||||
from django.test import TestCase
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..handlers import NotificationHandler
|
||||
from ..models import Notification
|
||||
|
||||
MODULE_PATH = 'allianceauth.notifications.handlers'
|
||||
|
||||
|
||||
class TestHandler(TestCase):
|
||||
def test_do_nothing_if_permission_does_not_exist(self):
|
||||
# given
|
||||
Permission.objects.get(codename="logging_notifications").delete()
|
||||
handler = NotificationHandler()
|
||||
record = LogRecord(
|
||||
name="name",
|
||||
level=DEBUG,
|
||||
pathname="pathname",
|
||||
lineno=42,
|
||||
msg="msg",
|
||||
args=[],
|
||||
exc_info=None,
|
||||
func="func"
|
||||
)
|
||||
# when
|
||||
handler.emit(record)
|
||||
# then
|
||||
self.assertEqual(Notification.objects.count(), 0)
|
||||
|
||||
def test_should_emit_message_to_users_with_permission_only(self):
|
||||
# given
|
||||
AuthUtils.create_user('Lex Luthor')
|
||||
user_permission = AuthUtils.create_user('Bruce Wayne')
|
||||
user_permission = AuthUtils.add_permission_to_user_by_name(
|
||||
"auth.logging_notifications", user_permission
|
||||
)
|
||||
group = Group.objects.create(name="Dummy Group")
|
||||
perm = Permission.objects.get(codename="logging_notifications")
|
||||
group.permissions.add(perm)
|
||||
user_group = AuthUtils.create_user('Peter Parker')
|
||||
user_group.groups.add(group)
|
||||
user_superuser = User.objects.create_superuser("Clark Kent")
|
||||
handler = NotificationHandler()
|
||||
record = LogRecord(
|
||||
name="name",
|
||||
level=DEBUG,
|
||||
pathname="pathname",
|
||||
lineno=42,
|
||||
msg="msg",
|
||||
args=[],
|
||||
exc_info=None,
|
||||
func="func"
|
||||
)
|
||||
# when
|
||||
handler.emit(record)
|
||||
# then
|
||||
self.assertEqual(Notification.objects.count(), 3)
|
||||
users = set(Notification.objects.values_list("user__pk", flat=True))
|
||||
self.assertSetEqual(
|
||||
users, {user_permission.pk, user_group.pk, user_superuser.pk}
|
||||
)
|
||||
notif = Notification.objects.first()
|
||||
self.assertEqual(notif.user, user_permission)
|
||||
self.assertEqual(notif.title, "DEBUG [func:42]")
|
||||
self.assertEqual(notif.level, "success")
|
||||
self.assertEqual(notif.message, "msg")
|
||||
@@ -64,6 +64,35 @@ class TestUserNotify(TestCase):
|
||||
self.assertEqual(obj.user, self.user)
|
||||
self.assertEqual(obj.title, title)
|
||||
self.assertEqual(obj.message, title)
|
||||
|
||||
def test_should_use_default_level_when_not_specified(self):
|
||||
# given
|
||||
title = 'dummy_title'
|
||||
message = 'dummy message'
|
||||
# when
|
||||
Notification.objects.notify_user(self.user, title, message)
|
||||
# then
|
||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||
obj = Notification.objects.first()
|
||||
self.assertEqual(obj.user, self.user)
|
||||
self.assertEqual(obj.title, title)
|
||||
self.assertEqual(obj.message, message)
|
||||
self.assertEqual(obj.level, Notification.Level.INFO)
|
||||
|
||||
def test_should_use_default_level_when_invalid_level_given(self):
|
||||
# given
|
||||
title = 'dummy_title'
|
||||
message = 'dummy message'
|
||||
level = "invalid"
|
||||
# when
|
||||
Notification.objects.notify_user(self.user, title, message, level)
|
||||
# then
|
||||
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||
obj = Notification.objects.first()
|
||||
self.assertEqual(obj.user, self.user)
|
||||
self.assertEqual(obj.title, title)
|
||||
self.assertEqual(obj.message, message)
|
||||
self.assertEqual(obj.level, Notification.Level.INFO)
|
||||
|
||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=3)
|
||||
def test_remove_when_too_many_notifications(self):
|
||||
|
||||
@@ -30,5 +30,10 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
for user in queryset:
|
||||
user.delete_user()
|
||||
|
||||
_username.short_description = 'Discord Username'
|
||||
_username.admin_order_field = 'username'
|
||||
_username.admin_order_field = 'username'
|
||||
@@ -83,10 +83,31 @@ class DiscordUserManager(models.Manager):
|
||||
if created is not False:
|
||||
if created is None:
|
||||
logger.debug(
|
||||
"User %s with Discord ID %s is already a member.",
|
||||
"User %s with Discord ID %s is already a member. Forcing a Refresh",
|
||||
user,
|
||||
user_id,
|
||||
)
|
||||
|
||||
# Force an update cause the discord API won't do it for us.
|
||||
if role_ids:
|
||||
role_ids = list(role_ids)
|
||||
|
||||
updated = bot_client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=user_id,
|
||||
role_ids=role_ids,
|
||||
nick=nickname
|
||||
)
|
||||
|
||||
if not updated:
|
||||
# Could not update the new user so fail.
|
||||
logger.warning(
|
||||
"Failed to add user %s with Discord ID %s to Discord server",
|
||||
user,
|
||||
user_id,
|
||||
)
|
||||
return False
|
||||
|
||||
self.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
|
||||
@@ -111,6 +111,40 @@ class TestAddUser(TestCase):
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
def test_can_activate_existing_user_with_roles_no_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
):
|
||||
roles = [
|
||||
create_matched_role(ROLE_ALPHA),
|
||||
create_matched_role(ROLE_BRAVO),
|
||||
create_matched_role(ROLE_CHARLIE)
|
||||
]
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = ['a', 'b', 'c']
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = roles
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
|
||||
def test_can_create_user_no_roles_with_nick(
|
||||
self,
|
||||
@@ -140,6 +174,36 @@ class TestAddUser(TestCase):
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
|
||||
def test_can_activate_existing_user_no_roles_with_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
):
|
||||
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False)
|
||||
def test_can_create_user_no_roles_and_without_nick_if_turned_off(
|
||||
self,
|
||||
@@ -183,6 +247,7 @@ class TestAddUser(TestCase):
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
self.assertTrue(result)
|
||||
@@ -190,6 +255,31 @@ class TestAddUser(TestCase):
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_can_activate_existing_guild_member_failure(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
):
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = False
|
||||
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_return_false_when_user_creation_fails(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.6 on 2021-03-23 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mumble', '0001_squashed_0011_auto_20201011_1009'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='mumbleuser',
|
||||
name='last_connect',
|
||||
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', max_length=254, null=True, verbose_name='Last Connection'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mumbleuser',
|
||||
name='last_disconnect',
|
||||
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection to Mumble', max_length=254, null=True, verbose_name='Last Disconnection'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mumbleuser',
|
||||
name='release',
|
||||
field=models.TextField(blank=True, editable=False, help_text='The Mumble Release the user last authenticated with', max_length=254, null=True, verbose_name='Mumble Release'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mumbleuser',
|
||||
name='version',
|
||||
field=models.IntegerField(blank=True, editable=False, help_text='Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.', null=True, verbose_name='Mumble Version'),
|
||||
),
|
||||
]
|
||||
@@ -74,7 +74,41 @@ class MumbleUser(AbstractServiceModel):
|
||||
editable=False,
|
||||
help_text="Hash of Mumble client certificate as presented when user authenticates"
|
||||
)
|
||||
display_name = models.CharField(max_length=254, unique=True)
|
||||
display_name = models.CharField(
|
||||
max_length=254,
|
||||
unique=True
|
||||
)
|
||||
release = models.TextField(
|
||||
verbose_name="Mumble Release",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="The Mumble Release the user last authenticated with"
|
||||
)
|
||||
version = models.IntegerField(
|
||||
verbose_name="Mumble Version",
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203."
|
||||
)
|
||||
last_connect = models.DateTimeField(
|
||||
verbose_name="Last Connection",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Timestamp of the users Last Connection to Mumble"
|
||||
)
|
||||
last_disconnect = models.DateTimeField(
|
||||
verbose_name="Last Disconnection",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Timestamp of the users Last Disconnection to Mumble"
|
||||
)
|
||||
|
||||
objects = MumbleManager()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class Teamspeak3UserAdmin(ServicesUserAdmin):
|
||||
|
||||
@admin.register(AuthTS)
|
||||
class AuthTSgroupAdmin(admin.ModelAdmin):
|
||||
change_list_template = 'admin/teamspeak3/authts/change_list.html'
|
||||
ordering = ('auth_group__name', )
|
||||
list_select_related = True
|
||||
|
||||
@@ -28,7 +29,7 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
|
||||
return [x for x in obj.ts_group.all().order_by('ts_group_id')]
|
||||
|
||||
_ts_group.short_description = 'ts groups'
|
||||
#_ts_group.admin_order_field = 'profile__state'
|
||||
# _ts_group.admin_order_field = 'profile__state'
|
||||
|
||||
|
||||
@admin.register(StateGroup)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
{{ block.super }}
|
||||
<li>
|
||||
<a href="{% url 'teamspeak3:admin_update_ts3_groups' %}" class="btn btn-high">
|
||||
Update TS3 groups
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
@@ -216,6 +216,24 @@ class Teamspeak3ViewsTestCase(TestCase):
|
||||
self.assertEqual(ts3_user.perm_key, '123abc')
|
||||
self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
|
||||
|
||||
@mock.patch(MODULE_PATH + '.views.Teamspeak3Tasks')
|
||||
@mock.patch(MODULE_PATH + '.views.messages')
|
||||
def test_should_update_ts_groups(self, messages, Teamspeak3Tasks):
|
||||
# given
|
||||
self.member.is_superuser = True
|
||||
self.member.is_staff = True
|
||||
self.member.save()
|
||||
self.login()
|
||||
# when
|
||||
response = self.client.get(urls.reverse('teamspeak3:admin_update_ts3_groups'))
|
||||
# then
|
||||
self.assertRedirects(
|
||||
response, urls.reverse('admin:teamspeak3_authts_changelist'),
|
||||
target_status_code=200
|
||||
)
|
||||
self.assertTrue(messages.info.called)
|
||||
self.assertTrue(Teamspeak3Tasks.run_ts3_group_update.delay.called)
|
||||
|
||||
|
||||
class Teamspeak3SignalsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -6,12 +6,14 @@ app_name = 'teamspeak3'
|
||||
|
||||
module_urls = [
|
||||
# Teamspeak3 service control
|
||||
url(r'^activate/$', views.activate_teamspeak3,
|
||||
name='activate'),
|
||||
url(r'^deactivate/$', views.deactivate_teamspeak3,
|
||||
name='deactivate'),
|
||||
url(r'^reset_perm/$', views.reset_teamspeak3_perm,
|
||||
name='reset_perm'),
|
||||
url(r'^activate/$', views.activate_teamspeak3, name='activate'),
|
||||
url(r'^deactivate/$', views.deactivate_teamspeak3, name='deactivate'),
|
||||
url(r'^reset_perm/$', views.reset_teamspeak3_perm, name='reset_perm'),
|
||||
url(
|
||||
r'^admin_update_ts3_groups/$',
|
||||
views.admin_update_ts3_groups,
|
||||
name='admin_update_ts3_groups'
|
||||
),
|
||||
|
||||
# Teamspeak Urls
|
||||
url(r'^verify/$', views.verify_teamspeak3, name='verify'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import render, redirect
|
||||
from django.conf import settings
|
||||
@@ -99,3 +100,11 @@ def reset_teamspeak3_perm(request):
|
||||
logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user)
|
||||
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
|
||||
return redirect("services:services")
|
||||
|
||||
|
||||
@login_required
|
||||
@staff_member_required
|
||||
def admin_update_ts3_groups(request):
|
||||
Teamspeak3Tasks.run_ts3_group_update.delay()
|
||||
messages.info(request, "Started updating TS3 server groups...")
|
||||
return redirect("admin:teamspeak3_authts_changelist")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -21,6 +23,11 @@ class SrpFleetUserRequestForm(forms.Form):
|
||||
data = self.cleaned_data['killboard_link']
|
||||
if "zkillboard.com" not in data:
|
||||
raise forms.ValidationError(_("Invalid Link. Please use zKillboard.com"))
|
||||
|
||||
if not re.match(r"http[s]?://zkillboard\.com/kill/\d+\/", data):
|
||||
raise forms.ValidationError(
|
||||
_("Invalid Link. Please post a direct link to a killmail.")
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
{% include 'bundles/x-editable.css.html' %}
|
||||
<link href="{% static 'css/checkbox.css' %}" rel="stylesheet" type="text/css">
|
||||
<style>
|
||||
.copy-text-fa-icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio label, .checkbox label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
@@ -109,7 +112,8 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
|
||||
{{ srpfleetrequest.character.alliance.alliance_ticker }}
|
||||
{% endif %}
|
||||
[{{ srpfleetrequest.character.corporation.corporation_ticker }}]
|
||||
{{ srpfleetrequest.character.character_name }}
|
||||
{{ srpfleetrequest.character.character_name }} <i class="copy-text-fa-icon far fa-copy" data-clipboard-text="{{ srpfleetrequest.character.character_name }}"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ srpfleetrequest.killboard_link }}"
|
||||
@@ -182,7 +186,24 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% include 'bundles/x-editable-js.html' %}
|
||||
{% include 'bundles/moment-js.html' %}
|
||||
{% endblock %}
|
||||
{% include 'bundles/clipboard-js.html' %}
|
||||
|
||||
<script>
|
||||
var clipboard = new ClipboardJS('.copy-text-fa-icon');
|
||||
clipboard.on('success', function (e) {
|
||||
console.info('Action:', e.action);
|
||||
console.info('Text:', e.text);
|
||||
console.info('Trigger:', e.trigger);
|
||||
|
||||
e.clearSelection();
|
||||
});
|
||||
|
||||
clipboard.on('error', function (e) {
|
||||
console.error('Action:', e.action);
|
||||
console.error('Trigger:', e.trigger);
|
||||
});
|
||||
</script>
|
||||
{% endblock extra_javascript %}
|
||||
|
||||
{% block extra_script %}
|
||||
$(document).ready(function() {
|
||||
|
||||
@@ -43,27 +43,51 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* style group headers within a table */
|
||||
.tr-group {
|
||||
font-weight: bold;
|
||||
background-color: #e6e6e6 !important;
|
||||
}
|
||||
@media all {
|
||||
/* style nav tabs in dark mode*/
|
||||
.template-dark-mode .nav-tabs > li.active > a {
|
||||
background-color: rgb(70, 69, 69)!important;
|
||||
color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
/* default style for tables */
|
||||
.table-aa > thead > tr > th{
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.table-aa > thead > tr > th{
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-aa > tbody > tr > td{
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.table-aa > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-aa > tbody > tr:last-child {
|
||||
border-bottom: none;
|
||||
.panel-tabs-aa {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0%;
|
||||
border-top-right-radius: 0%;
|
||||
}
|
||||
|
||||
/* style group headers within a table */
|
||||
.template-light-mode .tr-group {
|
||||
font-weight: bold;
|
||||
background-color: #e6e6e6 !important;
|
||||
}
|
||||
.template-dark-mode .tr-group {
|
||||
font-weight: bold;
|
||||
background-color: rgb(105, 105, 105) !important;
|
||||
}
|
||||
|
||||
/* default style for tables */
|
||||
.template-light-mode .table-aa > thead > tr > th{
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.template-dark-mode .table-aa > thead > tr > th{
|
||||
border-bottom: 1px solid rgb(70, 69, 69);
|
||||
}
|
||||
.table-aa > thead > tr > th{
|
||||
vertical-align: middle;
|
||||
}
|
||||
.template-light-mode .table-aa > tbody > tr > td{
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
.template-dark-mode .table-aa > tbody > tr > td{
|
||||
border-bottom: 1px solid rgb(70, 69, 69);
|
||||
}
|
||||
.table-aa > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-aa > tbody > tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* highlight active menu items
|
||||
|
||||
@@ -18,9 +18,20 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="text-right" style="position:absolute;bottom:5px;right:5px;">
|
||||
<a href="https://gitlab.com/allianceauth/allianceauth/issues"><span class="label" style="background-color:#e65328;">
|
||||
<i class="fab fa-gitlab" aria-hidden="true"></i> Powered by GitLab</span>
|
||||
|
||||
<div class="text-right" style="position: absolute; bottom: 5px; right: 5px;">
|
||||
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" style="margin-right: 0.5rem;">
|
||||
<span class="label" style="background-color: #e65328;">
|
||||
<i class="fab fa-gitlab" aria-hidden="true"></i>
|
||||
{% translate 'Powered by GitLab' %}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://discord.com/invite/fjnHAmk" target="_blank">
|
||||
<span class="label" style="background-color: rgb(110,133,211);">
|
||||
<i class="fab fa-discord" aria-hidden="true"></i>
|
||||
{% translate 'Support Discord' %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,15 +30,7 @@
|
||||
<div class="row" id="site-body-wrapper">
|
||||
{% include 'allianceauth/side-menu.html' %}
|
||||
<div class="col-sm-10">
|
||||
{% if messages %}
|
||||
<br>
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.level_tag }}">{{ message }}</div>
|
||||
{% if not forloop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include 'allianceauth/messages.html' %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
||||
26
allianceauth/templates/allianceauth/messages.html
Normal file
26
allianceauth/templates/allianceauth/messages.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% if messages %}
|
||||
<br>
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
<div class="message-icon pull-left" style="margin-right: 0.5rem;">
|
||||
{% if message.level_tag == "info" %}
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% elif message.level_tag == "success" %}
|
||||
<i class="fas fa-check-circle"></i>
|
||||
{% elif message.level_tag == "warning" %}
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
{% elif message.level_tag == "danger" %}
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="message-text" style="margin-left: 2.5rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -1,3 +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>
|
||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js" integrity="sha512-sIqUEnRn31BgngPmHt2JenzleDDsXwYO+iyvQ46Mw6RL+udAUZj2n/u/PGY80NxRxynO7R9xIGx5LEzw4INWJQ==" crossorigin="anonymous"></script>
|
||||
<!-- End Clipboard.js js from cdnjs -->
|
||||
@@ -70,8 +70,8 @@ def _current_notifications() -> dict:
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
NOTIFICATION_CACHE_TIME
|
||||
)
|
||||
except requests.RequestException:
|
||||
logger.exception('Error while getting gitlab notifications')
|
||||
except requests.HTTPError:
|
||||
logger.warning('Error while getting gitlab notifications', exc_info=True)
|
||||
top_notifications = []
|
||||
else:
|
||||
if notifications:
|
||||
@@ -95,8 +95,8 @@ def _current_version_summary() -> dict:
|
||||
tags = cache.get_or_set(
|
||||
'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME
|
||||
)
|
||||
except requests.RequestException:
|
||||
logger.exception('Error while getting gitlab release tags')
|
||||
except requests.HTTPError:
|
||||
logger.warning('Error while getting gitlab release tags', exc_info=True)
|
||||
return {}
|
||||
|
||||
if not tags:
|
||||
|
||||
@@ -23,7 +23,7 @@ Within your auth project exists two folders named `static` and `templates`. Thes
|
||||
|
||||
You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory.
|
||||
|
||||
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allinceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
|
||||
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allianceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
|
||||
|
||||
## Custom URLs and Views
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ The development environment consists of the following components:
|
||||
|
||||
We will use the build-in Django development webserver, so we don't need to setup a WSGI server or a web server.
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
This setup works with both WSL 1 and WSL 2. However, due to the significantly better performance we recommend WSL 2.
|
||||
```
|
||||
|
||||
## Requirement
|
||||
|
||||
The only requirement is a PC with Windows 10 and Internet connection in order to download the additional software components.
|
||||
@@ -361,8 +366,7 @@ Here is an example debug config for Celery:
|
||||
"module": "celery",
|
||||
"cwd": "${workspaceFolder}/myauth",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"-E",
|
||||
"args": [
|
||||
"-A",
|
||||
"myauth",
|
||||
"worker",
|
||||
@@ -371,7 +375,8 @@ Here is an example debug config for Celery:
|
||||
"-P",
|
||||
"solo",
|
||||
],
|
||||
"django": true
|
||||
"django": true,
|
||||
"justMyCode": true,
|
||||
},
|
||||
```
|
||||
|
||||
@@ -386,15 +391,14 @@ Finally it makes sense to have a dedicated debug config for running unit tests.
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/myauth/manage.py",
|
||||
"args": [
|
||||
"test",
|
||||
"-v 2",
|
||||
"test",
|
||||
"--keepdb",
|
||||
"--debug-mode",
|
||||
"--failfast",
|
||||
"example",
|
||||
],
|
||||
|
||||
"django": true
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
},
|
||||
```
|
||||
|
||||
@@ -416,13 +420,31 @@ Finally you may also want to have a debug config to debug a non-Django Python sc
|
||||
|
||||
## Additional tools
|
||||
|
||||
The following additional tools are very helpful when developing for AA.
|
||||
The following additional tools are very helpful when developing for AA with VS Code:
|
||||
|
||||
### Pylance
|
||||
|
||||
Pylance is an extension that works alongside Python in Visual Studio Code to provide performant language support: [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
|
||||
|
||||
### Code Spell Checker
|
||||
|
||||
Typos in your user facing comments can be quite embarrassing. This spell checker helps you avoid them.
|
||||
Typos in your user facing comments can be quite embarrassing. This spell checker helps you avoid them: [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
|
||||
|
||||
Install from here: [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
|
||||
### markdownlint
|
||||
|
||||
Extension for Visual Studio Code - Markdown linting and style checking for Visual Studio Code: [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
|
||||
|
||||
### GitLens
|
||||
|
||||
Extension for Visual Studio Code - Supercharge the Git capabilities built into Visual Studio Code: [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
|
||||
### RST preview
|
||||
|
||||
A VS Code extension to preview restructured text and provide syntax highlighting: [RST Preview](https://marketplace.visualstudio.com/items?itemName=tht13.rst-vscode)
|
||||
|
||||
### Django Template
|
||||
|
||||
This extension adds language colorization support and user snippets for the Django template language to VS Code: [Django Template](https://marketplace.visualstudio.com/items?itemName=bibhasdn.django-html)
|
||||
|
||||
### DBeaver
|
||||
|
||||
|
||||
@@ -7,19 +7,16 @@ The notifications package has an API for sending notifications.
|
||||
Location: ``allianceauth.notifications``
|
||||
|
||||
.. automodule:: allianceauth.notifications.__init__
|
||||
:members: notify
|
||||
:undoc-members:
|
||||
:members: notify
|
||||
|
||||
models
|
||||
===========
|
||||
|
||||
.. autoclass:: allianceauth.notifications.models.Notification
|
||||
:members: view, set_level, LEVEL_CHOICES
|
||||
:undoc-members:
|
||||
:members: Level, mark_viewed, set_level
|
||||
|
||||
managers
|
||||
===========
|
||||
|
||||
.. autoclass:: allianceauth.notifications.managers.NotificationManager
|
||||
:members: notify_user, user_unread_count
|
||||
:undoc-members:
|
||||
|
||||
@@ -42,11 +42,12 @@ Auto Groups are configured via models in the Admin Interface, a user will requir
|
||||
+-------------------------------------------+------------------+----------------+
|
||||
| Permission | Admin Site | Auth Site |
|
||||
+===========================================+==================+================+
|
||||
| eve_autogroups.add_autogroupsconfig | Can create model | None. |
|
||||
| eve_autogroups.add_autogroupsconfig | Can create model | None. |
|
||||
+-------------------------------------------+------------------+----------------+
|
||||
| eve_autogroups.change_autogroupsconfig | Can edit model | None. |
|
||||
| eve_autogroups.change_autogroupsconfig | Can edit model | None. |
|
||||
+-------------------------------------------+------------------+----------------+
|
||||
| eve_autogroups.delete_autogroupsconfig | Can delete model | None. |
|
||||
| eve_autogroups.delete_autogroupsconfig | Can delete model | None. |
|
||||
+-------------------------------------------+------------------+----------------+
|
||||
```
|
||||
|
||||
There exists more models that will be automatically created and maintained by this module, they do not require end-user/admin interaction. `managedalliancegroup` `managedcorpgroups`
|
||||
|
||||
@@ -26,7 +26,7 @@ DISCORD_SYNC_NAMES = False
|
||||
|
||||
CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
|
||||
'task': 'discord.update_all_usernames',
|
||||
'schedule': crontab(hour='*/12'),
|
||||
'schedule': crontab(minute=0, hour='*/12'),
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -38,14 +38,14 @@ To install we need a copy of the server. You can find the latest version from [t
|
||||
|
||||
Download the server, replacing the link with the link you got earlier.
|
||||
|
||||
```url
|
||||
```text
|
||||
http://dl.4players.de/ts/releases/3.13.2/teamspeak3-server_linux_amd64-3.13.2.tar.bz2
|
||||
```
|
||||
|
||||
Now we need to extract the file.
|
||||
|
||||
```bash
|
||||
tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2
|
||||
tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2
|
||||
```
|
||||
|
||||
### Create User
|
||||
@@ -105,7 +105,9 @@ Click the URL provided to automatically connect to our server. It will prompt yo
|
||||
|
||||
Now we need to make groups. AllianceAuth handles groups in teamspeak differently: instead of creating groups it creates an association between groups in TeamSpeak and groups in AllianceAuth. Go ahead and make the groups you want to associate with auth groups, keeping in mind multiple TeamSpeak groups can be associated with a single auth group.
|
||||
|
||||
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Services`, select `Auth / TS Groups`. In the top-right corner click `Add`.
|
||||
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Teamspeak3`, select `Auth / TS Groups`.
|
||||
|
||||
In the top-right corner click, first click on `Update TS3 Groups` to fetch the newly created server groups from TS3 (this may take a minute to complete). Then click on `Add Auth / TS Group` to link Auth groups with TS3 server groups.
|
||||
|
||||
The dropdown box provides all auth groups. Select one and assign TeamSpeak groups from the panels below. If these panels are empty, wait a minute for the database update to run, or see the [troubleshooting section](#ts-group-models-not-populating-on-admin-site) below.
|
||||
|
||||
@@ -119,13 +121,23 @@ To enable advanced permissions, on your client go to the `Tools` menu, `Applicat
|
||||
|
||||
### TS group models not populating on admin site
|
||||
|
||||
The method which populates these runs every 30 minutes. To populate manually, start a django shell:
|
||||
The method which populates these runs every 30 minutes. To populate manually you start the process from the admin site or from the Django shell.
|
||||
|
||||
#### Admin Site
|
||||
|
||||
Navigate to the AllianceAuth admin interface and under `Teamspeak3`, select `Auth / TS Groups`.
|
||||
|
||||
Then, in the top-right corner click, click on `Update TS3 Groups` to start the process of fetching the server groups from TS3 (this may take a minute to complete).
|
||||
|
||||
#### Django Shell
|
||||
|
||||
Start a django shell with:
|
||||
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
And execute the update:
|
||||
And execute the update as follows:
|
||||
|
||||
```python
|
||||
from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks
|
||||
|
||||
@@ -104,6 +104,8 @@ CREATE DATABASE alliance_auth CHARACTER SET utf8mb4;
|
||||
GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost';
|
||||
```
|
||||
|
||||
Once your database is set up, you can leave the SQL shell with `exit`.
|
||||
|
||||
Add timezone tables to your mysql installation:
|
||||
|
||||
```bash
|
||||
@@ -177,7 +179,7 @@ source /home/allianceserver/venv/auth/bin/activate
|
||||
|
||||
You need to have a dedicated Eve SSO app for Alliance auth. Please go to [EVE Developer](https://developers.eveonline.com/applications) to create one.
|
||||
|
||||
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
|
||||
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience, we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
|
||||
|
||||
As **callback URL** you want to define the URL of your Alliance Auth site plus the route: `/sso/callback`. Example for a valid callback URL: `https://auth.example.com/sso/callback`
|
||||
|
||||
@@ -236,7 +238,7 @@ Check to ensure your settings are valid.
|
||||
python /home/allianceserver/myauth/manage.py check
|
||||
```
|
||||
|
||||
And finally ensure the allianceserver user has read/write permissions to this directory before proceeding.
|
||||
Finally, ensure the allianceserver user has read/write permissions to this directory before proceeding.
|
||||
|
||||
```bash
|
||||
chown -R allianceserver:allianceserver /home/allianceserver/myauth
|
||||
@@ -244,7 +246,7 @@ chown -R allianceserver:allianceserver /home/allianceserver/myauth
|
||||
|
||||
## Services
|
||||
|
||||
Alliance Auth needs some additional services to run, which we will setup and configure next.
|
||||
Alliance Auth needs some additional services to run, which we will set up and configure next.
|
||||
|
||||
### Gunicorn
|
||||
|
||||
@@ -275,7 +277,7 @@ systemctl enable supervisord.service
|
||||
systemctl start supervisord.service
|
||||
```
|
||||
|
||||
Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
|
||||
Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
|
||||
|
||||
Ubuntu:
|
||||
|
||||
@@ -289,7 +291,7 @@ CentOS:
|
||||
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
|
||||
```
|
||||
|
||||
And activate it with `supervisorctl reload`.
|
||||
Activate it with `supervisorctl reload`.
|
||||
|
||||
You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process.
|
||||
|
||||
@@ -304,11 +306,11 @@ You can check the status of the processes with `supervisorctl status`. Logs from
|
||||
|
||||
Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide.
|
||||
|
||||
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use and some functions (e.g. Email confirmation links) will not work properly.
|
||||
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use, and some functions (e.g. Email confirmation links) will not work properly.
|
||||
|
||||
## Superuser
|
||||
|
||||
Before using your auth site it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
|
||||
Before using your auth site, it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
|
||||
|
||||
```bash
|
||||
python /home/allianceserver/myauth/manage.py createsuperuser
|
||||
@@ -316,7 +318,7 @@ python /home/allianceserver/myauth/manage.py createsuperuser
|
||||
|
||||
The superuser account is accessed by logging in via the admin site at `https://example.com/admin`.
|
||||
|
||||
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added it is possible to use SSO to login to this account.
|
||||
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added, it is possible to use SSO to login to this account.
|
||||
|
||||
## Updating
|
||||
|
||||
@@ -340,7 +342,7 @@ Some releases come with new or changed models. Update your database to reflect t
|
||||
python /home/allianceserver/myauth/manage.py migrate
|
||||
```
|
||||
|
||||
Finally some releases come with new or changed static files. Run the following command to update your static files folder:
|
||||
Finally, some releases come with new or changed static files. Run the following command to update your static files folder:
|
||||
|
||||
```bash
|
||||
python /home/allianceserver/myauth/manage.py collectstatic
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Adding and Removing Apps
|
||||
# App Maintenance
|
||||
|
||||
## Adding and Removing Apps
|
||||
|
||||
Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but here is the general procedure:
|
||||
|
||||
@@ -8,3 +10,17 @@ Your auth project is just a regular Django project - you can add in [other Djang
|
||||
4. restart AA with `supervisorctl restart myauth:`
|
||||
|
||||
If you ever want to remove an app, you should first clear it from the database to avoid dangling foreign keys: `python manage.py migrate appname zero`. Then you can remove it from your auth project's `INSTALLED_APPS` list.
|
||||
|
||||
## Permission Cleanup
|
||||
|
||||
Mature Alliance Auth installations, or those with actively developed extensions may find themselves with stale or duplicated Permission models.
|
||||
|
||||
This can make it confusing for admins to apply the right permissions, contribute to larger queries in backend management or simply look unsightly.
|
||||
|
||||
```shell
|
||||
python manage.py remove_stale_contenttypes --include-stale-apps
|
||||
```
|
||||
|
||||
This inbuilt Django command will step through each contenttype and offer to delete it, displaying what exactly this will cascade to delete. Pay attention and ensure you understand exactly what is being removed before answering `yes`.
|
||||
|
||||
This should only cleanup uninstalled apps, deprecated permissions within apps should be cleaned up using Data Migrations by each responsible application.
|
||||
|
||||
8
setup.py
8
setup.py
@@ -22,18 +22,18 @@ install_requires = [
|
||||
'celery>=4.3.0,<5.0.0,!=4.4.4', # 4.4.4 is missing a dependency
|
||||
'celery_once>=2.0.1',
|
||||
|
||||
'django>=3.1.1,<4.0.0',
|
||||
'django>=3.1.1,<3.2.0',
|
||||
'django-bootstrap-form',
|
||||
'django-registration>=3.1',
|
||||
'django-sortedm2m',
|
||||
'django-redis-cache>=3.0.0',
|
||||
'django-celery-beat>=2.0.0',
|
||||
'django-celery-beat>=2.0.0,<2.2.1',
|
||||
|
||||
'openfire-restapi',
|
||||
'sleekxmpp',
|
||||
'pydiscourse',
|
||||
|
||||
'django-esi>=2.0.4,<3.0'
|
||||
'django-esi>=3.0.0,<4.0.0'
|
||||
]
|
||||
|
||||
testing_extras = [
|
||||
@@ -71,7 +71,7 @@ setup(
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 2.2',
|
||||
'Framework :: Django :: 3.1',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
|
||||
Reference in New Issue
Block a user