diff --git a/allianceauth/authentication/admin.py b/allianceauth/authentication/admin.py
index 894ab2f5..0393168e 100644
--- a/allianceauth/authentication/admin.py
+++ b/allianceauth/authentication/admin.py
@@ -1,15 +1,33 @@
+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
-from django.utils.text import slugify
-from django.db.models import Q
+from django.contrib.auth.models import User as BaseUser, \
+ Permission as BasePermission, Group
+from django.db.models import Q, F
from allianceauth.services.hooks import ServicesHook
-from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
+from django.db.models.signals import pre_save, post_save, pre_delete, \
+ post_delete, m2m_changed
+from django.db.models.functions import Lower
from django.dispatch import receiver
-from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
-from allianceauth.hooks import get_hooks
-from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm
+from django.utils.html import format_html
+from django.urls import reverse
+from django.utils.text import slugify
+
+from allianceauth.authentication.models import State, get_guest_state,\
+ CharacterOwnership, UserProfile, OwnershipRecord
+from allianceauth.hooks import get_hooks
+from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
+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
+ from allianceauth.eveonline.autogroups.models import *
+else:
+ _has_auto_groups = False
def make_service_hooks_update_groups_action(service):
@@ -83,41 +101,323 @@ class UserProfileInline(admin.StackedInline):
return False
+def user_profile_pic(obj):
+ """profile pic column data for user objects
+
+ works for both User objects and objects with `user` as FK to User
+ To be used for all user based admin lists (requires CSS)
+ """
+ user_obj = obj.user if hasattr(obj, 'user') else obj
+ if user_obj.profile.main_character:
+ return format_html(
+ '
',
+ user_obj.profile.main_character.portrait_url(size=32)
+ )
+ else:
+ return None
+user_profile_pic.short_description = ''
+
+
+def user_username(obj):
+ """user column data for user objects
+
+ works for both User objects and objects with `user` as FK to User
+ To be used for all user based admin lists
+ """
+ link = reverse(
+ 'admin:{}_{}_change'.format(
+ obj._meta.app_label,
+ type(obj).__name__.lower()
+ ),
+ args=(obj.pk,)
+ )
+ user_obj = obj.user if hasattr(obj, 'user') else obj
+ if user_obj.profile.main_character:
+ return format_html(
+ '{}
{}',
+ link,
+ user_obj.username,
+ user_obj.profile.main_character.character_name
+ )
+ else:
+ return format_html(
+ '{}',
+ link,
+ user_obj.username,
+ )
+
+user_username.short_description = 'user / main'
+user_username.admin_order_field = 'username'
+
+
+def user_main_organization(obj):
+ """main organization column data for user objects
+
+ works for both User objects and objects with `user` as FK to User
+ To be used for all user based admin lists
+ """
+ user_obj = obj.user if hasattr(obj, 'user') else obj
+ if not user_obj.profile.main_character:
+ result = None
+ else:
+ corporation = user_obj.profile.main_character.corporation_name
+ if user_obj.profile.main_character.alliance_id:
+ result = format_html('{}
{}',
+ corporation,
+ user_obj.profile.main_character.alliance_name
+ )
+ else:
+ result = corporation
+ return result
+
+user_main_organization.short_description = 'Corporation / Alliance (Main)'
+user_main_organization.admin_order_field = \
+ 'profile__main_character__corporation_name'
+
+
+class MainCorporationsFilter(admin.SimpleListFilter):
+ """Custom filter to filter on corporations from mains only
+
+ works for both User objects and objects with `user` as FK to User
+ To be used for all user based admin lists
+ """
+ title = 'corporation'
+ parameter_name = 'main_corporation_id__exact'
+
+ def lookups(self, request, model_admin):
+ qs = EveCharacter.objects\
+ .exclude(userprofile=None)\
+ .values('corporation_id', 'corporation_name')\
+ .distinct()\
+ .order_by(Lower('corporation_name'))
+ return tuple(
+ [(x['corporation_id'], x['corporation_name']) for x in qs]
+ )
+
+ def queryset(self, request, qs):
+ if self.value() is None:
+ return qs.all()
+ else:
+ if qs.model == User:
+ return qs\
+ .filter(profile__main_character__corporation_id=\
+ self.value())
+ else:
+ return qs\
+ .filter(user__profile__main_character__corporation_id=\
+ self.value())
+
+
+class MainAllianceFilter(admin.SimpleListFilter):
+ """Custom filter to filter on alliances from mains only
+
+ works for both User objects and objects with `user` as FK to User
+ To be used for all user based admin lists
+ """
+ title = 'alliance'
+ parameter_name = 'main_alliance_id__exact'
+
+ def lookups(self, request, model_admin):
+ qs = EveCharacter.objects\
+ .exclude(alliance_id=None)\
+ .exclude(userprofile=None)\
+ .values('alliance_id', 'alliance_name')\
+ .distinct()\
+ .order_by(Lower('alliance_name'))
+ return tuple(
+ [(x['alliance_id'], x['alliance_name']) for x in qs]
+ )
+
+ def queryset(self, request, qs):
+ if self.value() is None:
+ return qs.all()
+ else:
+ if qs.model == User:
+ return qs\
+ .filter(profile__main_character__alliance_id=self.value())
+ else:
+ return qs\
+ .filter(user__profile__main_character__alliance_id=\
+ self.value())
+
+
class UserAdmin(BaseUserAdmin):
+ """Extending Django's UserAdmin model
+
+ Behavior of groups and characters columns can be configured via settings
+
"""
- Extending Django's UserAdmin model
- """
+
+ 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 update_main_character_model(self, request, queryset):
+ tasks_count = 0
+ for obj in queryset:
+ if obj.profile.main_character:
+ update_character.delay(obj.profile.main_character.character_id)
+ tasks_count += 1
+
+ self.message_user(
+ request,
+ 'Update from ESI started for {} characters'.format(tasks_count)
+ )
+
+ update_main_character_model.short_description = \
+ 'Update main character model from ESI'
+
def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request)
+ actions[self.update_main_character_model.__name__] = (
+ self.update_main_character_model,
+ self.update_main_character_model.__name__,
+ self.update_main_character_model.short_description
+ )
+
for hook in get_hooks('services_hook'):
svc = hook()
# Check update_groups is redefined/overloaded
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
action = make_service_hooks_update_groups_action(svc)
- actions[action.__name__] = (action,
- action.__name__,
- action.short_description)
+ actions[action.__name__] = (
+ action,
+ action.__name__,
+ action.short_description
+ )
+
# Create sync nickname action if service implements it
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
action = make_service_hooks_sync_nickname_action(svc)
- actions[action.__name__] = (action,
- action.__name__,
- action.short_description)
-
+ actions[action.__name__] = (
+ action, action.__name__,
+ action.short_description
+ )
return actions
- list_filter = BaseUserAdmin.list_filter + ('profile__state',)
+
+ def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str:
+ """converts list of strings into HTML with cutoff and tooltip"""
+ items_truncated_str = ', '.join(my_items[:max_items])
+ if not my_items:
+ result = None
+ elif len(my_items) <= max_items:
+ result = items_truncated_str
+ else:
+ items_truncated_str += ', (...)'
+ items_all_str = ', '.join(my_items)
+ result = format_html(
+ '{}',
+ items_all_str,
+ items_truncated_str
+ )
+ return result
+
inlines = BaseUserAdmin.inlines + [UserProfileInline]
- list_display = ('username', 'email', 'get_main_character', 'get_state', 'is_active')
- def get_main_character(self, obj):
- return obj.profile.main_character
- get_main_character.short_description = "Main Character"
+ ordering = ('username', )
+ list_select_related = True
+ show_full_result_count = True
+
+ list_display = (
+ user_profile_pic,
+ user_username,
+ '_state',
+ '_groups',
+ user_main_organization,
+ '_characters',
+ 'is_active',
+ 'date_joined',
+ '_role'
+ )
+ list_display_links = None
- def get_state(self, obj):
- return obj.profile.state
- get_state.short_description = "State"
+ list_filter = (
+ 'profile__state',
+ RealGroupsFilter,
+ MainCorporationsFilter,
+ MainAllianceFilter,
+ 'is_active',
+ 'date_joined',
+ 'is_staff',
+ 'is_superuser'
+ )
+ search_fields = (
+ 'username',
+ 'character_ownerships__character__character_name'
+ )
+
+ 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()
+ ]
+ return self._list_2_html_w_tooltips(
+ my_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')
+ ]
+
+ return self._list_2_html_w_tooltips(
+ my_groups,
+ AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
+ )
+
+ _groups.short_description = 'groups'
+
+ def _role(self, obj):
+ if obj.is_superuser:
+ role = 'Superuser'
+ elif obj.is_staff:
+ role = 'Staff'
+ else:
+ role = 'User'
+ return role
+
+ _role.short_description = 'role'
+
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user')
@@ -127,19 +427,54 @@ class UserAdmin(BaseUserAdmin):
def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user')
+ 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'))
+ return super().formfield_for_manytomany(db_field, request, **kwargs)
+
@admin.register(State)
-class StateAdmin(admin.ModelAdmin):
+class StateAdmin(admin.ModelAdmin):
+ list_select_related = True
+ list_display = ('name', 'priority', '_user_count')
+
+ def _user_count(self, obj):
+ return obj.userprofile_set.all().count()
+ _user_count.short_description = 'Users'
+
fieldsets = (
(None, {
'fields': ('name', 'permissions', 'priority'),
}),
('Membership', {
- 'fields': ('public', 'member_characters', 'member_corporations', 'member_alliances'),
+ 'fields': (
+ 'public',
+ 'member_characters',
+ 'member_corporations',
+ 'member_alliances'
+ ),
})
)
- filter_horizontal = ['member_characters', 'member_corporations', 'member_alliances', 'permissions']
- list_display = ('name', 'priority', 'user_count')
+ filter_horizontal = [
+ 'member_characters',
+ 'member_corporations',
+ 'member_alliances',
+ 'permissions'
+ ]
+
+ def formfield_for_manytomany(self, db_field, request, **kwargs):
+ """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'))
+ elif db_field.name == "member_corporations":
+ kwargs["queryset"] = EveCorporationInfo.objects.all()\
+ .order_by(Lower('corporation_name'))
+ elif db_field.name == "member_alliances":
+ kwargs["queryset"] = EveAllianceInfo.objects.all()\
+ .order_by(Lower('alliance_name'))
+ return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None):
if obj == get_guest_state():
@@ -154,15 +489,31 @@ class StateAdmin(admin.ModelAdmin):
}),
)
return super(StateAdmin, self).get_fieldsets(request, obj=obj)
-
- @staticmethod
- def user_count(obj):
- return obj.userprofile_set.all().count()
-
+
class BaseOwnershipAdmin(admin.ModelAdmin):
- list_display = ('user', 'character')
- search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
+ class Media:
+ css = {
+ "all": ("authentication/css/admin.css",)
+ }
+
+ list_select_related = True
+ list_display = (
+ user_profile_pic,
+ user_username,
+ user_main_organization,
+ 'character',
+ )
+ search_fields = (
+ 'user__user',
+ 'character__character_name',
+ 'character__corporation_name',
+ 'character__alliance_name'
+ )
+ list_filter = (
+ MainCorporationsFilter,
+ MainAllianceFilter,
+ )
def get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
diff --git a/allianceauth/authentication/app_settings.py b/allianceauth/authentication/app_settings.py
new file mode 100644
index 00000000..9494953f
--- /dev/null
+++ b/allianceauth/authentication/app_settings.py
@@ -0,0 +1,46 @@
+from django.conf import settings
+
+
+def _clean_setting(
+ name: str,
+ default_value: object,
+ min_value: int = None,
+ max_value: int = None,
+ required_type: type = None
+):
+ """cleans the input for a custom setting
+
+ Will use `default_value` if settings does not exit or has the wrong type
+ or is outside define boundaries (for int only)
+
+ Need to define `required_type` if `default_value` is `None`
+
+ Will assume `min_value` of 0 for int (can be overriden)
+
+ Returns cleaned value for setting
+ """
+ if default_value is None and not required_type:
+ raise ValueError('You must specify a required_type for None defaults')
+
+ if not required_type:
+ required_type = type(default_value)
+
+ if min_value is None and required_type == int:
+ min_value = 0
+
+ if (hasattr(settings, name)
+ and isinstance(getattr(settings, name), required_type)
+ and (min_value is None or getattr(settings, name) >= min_value)
+ and (max_value is None or getattr(settings, name) <= max_value)
+ ):
+ return getattr(settings, name)
+ else:
+ return default_value
+
+
+AUTHENTICATION_ADMIN_USERS_MAX_GROUPS = \
+ _clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_GROUPS', 10)
+
+AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \
+ _clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5)
+
diff --git a/allianceauth/authentication/static/authentication/css/admin.css b/allianceauth/authentication/static/authentication/css/admin.css
new file mode 100644
index 00000000..489edd2e
--- /dev/null
+++ b/allianceauth/authentication/static/authentication/css/admin.css
@@ -0,0 +1,29 @@
+/*
+CSS for allianceauth admin site
+*/
+
+/* styling for profile pic */
+.img-circle {
+ border-radius: 50%;
+}
+.column-user_profile_pic {
+ width: 1px;
+ white-space: nowrap;
+}
+
+/* tooltip */
+.tooltip {
+ position: relative ;
+}
+.tooltip:hover::after {
+ content: attr(data-tooltip) ;
+ position: absolute ;
+ top: 1.1em ;
+ left: 1em ;
+ min-width: 200px ;
+ border: 1px #808080 solid ;
+ padding: 8px ;
+ color: black ;
+ background-color: rgb(255, 255, 204) ;
+ z-index: 1 ;
+}
\ No newline at end of file
diff --git a/allianceauth/authentication/tests/__init__.py b/allianceauth/authentication/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/allianceauth/authentication/tests/test_admin.py b/allianceauth/authentication/tests/test_admin.py
new file mode 100644
index 00000000..4e94434e
--- /dev/null
+++ b/allianceauth/authentication/tests/test_admin.py
@@ -0,0 +1,439 @@
+from unittest.mock import patch
+
+from django.test import TestCase, RequestFactory
+from django.contrib import admin
+from django.contrib.admin.sites import AdminSite
+from django.contrib.auth.models import User as BaseUser, Group
+
+from allianceauth.authentication.models import CharacterOwnership, State
+from allianceauth.eveonline.autogroups.models import AutogroupsConfig
+from allianceauth.eveonline.models import (
+ EveCharacter, EveCorporationInfo, EveAllianceInfo
+)
+
+from ..admin import (
+ BaseUserAdmin,
+ MainCorporationsFilter,
+ MainAllianceFilter,
+ User,
+ UserAdmin,
+ user_main_organization,
+ user_profile_pic,
+ user_username,
+)
+
+
+MODULE_PATH = 'allianceauth.authentication.admin'
+
+
+class MockRequest(object):
+
+ def __init__(self, user=None):
+ self.user = user
+
+
+class TestUserAdmin(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # groups
+ cls.group_1 = Group.objects.create(
+ name='Group 1'
+ )
+ cls.group_2 = Group.objects.create(
+ name='Group 2'
+ )
+
+ # user 1 - corp and alliance, normal user
+ cls.character_1 = EveCharacter.objects.create(
+ character_id='1001',
+ character_name='Bruce Wayne',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ cls.character_1a = EveCharacter.objects.create(
+ character_id='1002',
+ character_name='Batman',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ alliance = EveAllianceInfo.objects.create(
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ executor_corp_id='2001'
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ member_count=42,
+ alliance=alliance
+ )
+ cls.user_1 = User.objects.create_user(
+ cls.character_1.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1,
+ owner_hash='x1' + cls.character_1.character_name,
+ user=cls.user_1
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1a,
+ owner_hash='x1' + cls.character_1a.character_name,
+ user=cls.user_1
+ )
+ cls.user_1.profile.main_character = cls.character_1
+ cls.user_1.profile.save()
+ cls.user_1.groups.add(cls.group_1)
+
+ # user 2 - corp only, staff
+ cls.character_2 = EveCharacter.objects.create(
+ character_id=1003,
+ character_name='Clark Kent',
+ corporation_id=2002,
+ corporation_name='Daily Planet',
+ corporation_ticker='DP',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2002,
+ corporation_name='Daily Plane',
+ corporation_ticker='DP',
+ member_count=99,
+ alliance=None
+ )
+ cls.user_2 = User.objects.create_user(
+ cls.character_2.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_2,
+ owner_hash='x1' + cls.character_2.character_name,
+ user=cls.user_2
+ )
+ cls.user_2.profile.main_character = cls.character_2
+ cls.user_2.profile.save()
+ cls.user_2.groups.add(cls.group_2)
+ cls.user_2.is_staff = True
+ cls.user_2.save()
+
+ # user 3 - no main, no group, superuser
+ cls.character_3 = EveCharacter.objects.create(
+ character_id=1101,
+ character_name='Lex Luthor',
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ member_count=666,
+ alliance=None
+ )
+ EveAllianceInfo.objects.create(
+ alliance_id='3101',
+ alliance_name='Lex World Domination',
+ alliance_ticker='LWD',
+ executor_corp_id=''
+ )
+ cls.user_3 = User.objects.create_user(
+ cls.character_3.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_3,
+ owner_hash='x1' + cls.character_3.character_name,
+ user=cls.user_3
+ )
+ cls.user_3.is_superuser = True
+ cls.user_3.save()
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.modeladmin = UserAdmin(
+ model=User, admin_site=AdminSite()
+ )
+
+ def _create_autogroups(self):
+ """create autogroups for corps and alliances"""
+ 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 = ('
')
+ self.assertEqual(user_profile_pic(self.user_1), expected)
+
+ def test_user_profile_pic_u3(self):
+ self.assertIsNone(user_profile_pic(self.user_3))
+
+ def test_user_username_u1(self):
+ expected = (
+ ''
+ 'Bruce_Wayne
Bruce Wayne'.format(self.user_1.pk)
+ )
+ self.assertEqual(user_username(self.user_1), expected)
+
+ def test_user_username_u3(self):
+ expected = (
+ ''
+ 'Lex_Luthor'.format(self.user_3.pk)
+ )
+ self.assertEqual(user_username(self.user_3), expected)
+
+ def test_user_main_organization_u1(self):
+ expected = 'Wayne Technologies
Wayne Enterprises'
+ self.assertEqual(user_main_organization(self.user_1), expected)
+
+ def test_user_main_organization_u2(self):
+ expected = 'Daily Planet'
+ self.assertEqual(user_main_organization(self.user_2), expected)
+
+ def test_user_main_organization_u3(self):
+ expected = None
+ self.assertEqual(user_main_organization(self.user_3), expected)
+
+ def test_characters_u1(self):
+ expected = 'Batman, Bruce Wayne'
+ result = self.modeladmin._characters(self.user_1)
+ self.assertEqual(result, expected)
+
+ def test_characters_u2(self):
+ expected = 'Clark Kent'
+ result = self.modeladmin._characters(self.user_2)
+ self.assertEqual(result, expected)
+
+ def test_characters_u3(self):
+ expected = 'Lex Luthor'
+ result = self.modeladmin._characters(self.user_3)
+ self.assertEqual(result, expected)
+
+ def test_groups_u1(self):
+ self._create_autogroups()
+ expected = 'Group 1'
+ result = self.modeladmin._groups(self.user_1)
+ self.assertEqual(result, expected)
+
+ def test_groups_u2(self):
+ self._create_autogroups()
+ 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):
+ result = self.modeladmin._groups(self.user_3)
+ self.assertIsNone(result)
+
+ def test_state(self):
+ expected = 'Guest'
+ result = self.modeladmin._state(self.user_1)
+ self.assertEqual(result, expected)
+
+ def test_role_u1(self):
+ expected = 'User'
+ result = self.modeladmin._role(self.user_1)
+ self.assertEqual(result, expected)
+
+ def test_role_u2(self):
+ expected = 'Staff'
+ result = self.modeladmin._role(self.user_2)
+ self.assertEqual(result, expected)
+
+ def test_role_u3(self):
+ expected = 'Superuser'
+ result = self.modeladmin._role(self.user_3)
+ self.assertEqual(result, expected)
+
+ def test_list_2_html_w_tooltips_no_cutoff(self):
+ items = ['one', 'two', 'three']
+ expected = 'one, two, three'
+ result = self.modeladmin._list_2_html_w_tooltips(items, 5)
+ self.assertEqual(expected, result)
+
+ def test_list_2_html_w_tooltips_w_cutoff(self):
+ items = ['one', 'two', 'three']
+ expected = ('one, two, (...)')
+ result = self.modeladmin._list_2_html_w_tooltips(items, 2)
+ self.assertEqual(expected, result)
+
+ def test_list_2_html_w_tooltips_empty_list(self):
+ items = []
+ expected = None
+ result = self.modeladmin._list_2_html_w_tooltips(items, 5)
+ self.assertEqual(expected, result)
+
+ # actions
+
+ @patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True)
+ @patch(MODULE_PATH + '.update_character')
+ def test_action_update_main_character_model(
+ self, mock_task, mock_message_user
+ ):
+ users_qs = User.objects.filter(pk__in=[self.user_1.pk, self.user_2.pk])
+ self.modeladmin.update_main_character_model(
+ MockRequest(self.user_1), users_qs
+ )
+ self.assertEqual(mock_task.delay.call_count, 2)
+ 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):
+ list_filter = (MainCorporationsFilter,)
+
+ 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 = [
+ ('2002', 'Daily Planet'),
+ ('2001', 'Wayne Technologies'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned
+ request = self.factory.get(
+ '/',
+ {'main_corporation_id__exact': self.character_1.corporation_id}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [self.user_1]
+ self.assertSetEqual(set(queryset), set(expected))
+
+ def test_filter_main_alliances(self):
+
+ class UserAdminTest(BaseUserAdmin):
+ list_filter = (MainAllianceFilter,)
+
+ 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 = [
+ ('3001', 'Wayne Enterprises'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned
+ request = self.factory.get(
+ '/',
+ {'main_alliance_id__exact': self.character_1.alliance_id}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [self.user_1]
+ self.assertSetEqual(set(queryset), set(expected))
\ No newline at end of file
diff --git a/allianceauth/authentication/tests.py b/allianceauth/authentication/tests/test_all.py
similarity index 98%
rename from allianceauth/authentication/tests.py
rename to allianceauth/authentication/tests/test_all.py
index 5b763b28..2d7b9ec2 100644
--- a/allianceauth/authentication/tests.py
+++ b/allianceauth/authentication/tests/test_all.py
@@ -1,23 +1,28 @@
from unittest import mock
from io import StringIO
-from django.test import TestCase
-from django.contrib.auth.models import User
-from allianceauth.tests.auth_utils import AuthUtils
-from .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
-from .backends import StateBackend
-from .tasks import check_character_ownership
-from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
-from esi.models import Token
-from esi.errors import IncompleteResponseError
-from allianceauth.authentication.decorators import main_character_required
-from django.test.client import RequestFactory
-from django.http.response import HttpResponse
-from django.contrib.auth.models import AnonymousUser
-from django.conf import settings
-from django.shortcuts import reverse
-from django.core.management import call_command
from urllib import parse
+from django.conf import settings
+from django.contrib.auth.models import AnonymousUser, User
+from django.core.management import call_command
+from django.http.response import HttpResponse
+from django.shortcuts import reverse
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+
+from allianceauth.authentication.decorators import main_character_required
+from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
+ EveAllianceInfo
+from allianceauth.tests.auth_utils import AuthUtils
+from esi.errors import IncompleteResponseError
+from esi.models import Token
+
+from ..backends import StateBackend
+from ..models import CharacterOwnership, UserProfile, State, get_guest_state,\
+ OwnershipRecord
+from ..tasks import check_character_ownership
+
MODULE_PATH = 'allianceauth.authentication'
diff --git a/allianceauth/authentication/tests/test_app_settings.py b/allianceauth/authentication/tests/test_app_settings.py
new file mode 100644
index 00000000..6bd2b8c1
--- /dev/null
+++ b/allianceauth/authentication/tests/test_app_settings.py
@@ -0,0 +1,108 @@
+from unittest.mock import Mock, patch
+
+from django.test import TestCase
+
+from .. import app_settings
+
+MODULE_PATH = 'allianceauth.authentication'
+
+class TestSetAppSetting(TestCase):
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_if_not_set(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ False,
+ )
+ self.assertEqual(result, False)
+
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_if_not_set_for_none(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ None,
+ required_type=int
+ )
+ self.assertEqual(result, None)
+
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_true_stays_true(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = True
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ False,
+ )
+ self.assertEqual(result, True)
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_false_stays_false(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = False
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ False
+ )
+ self.assertEqual(result, False)
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_for_invalid_type_bool(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = 'invalid type'
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ False
+ )
+ self.assertEqual(result, False)
+
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_for_invalid_type_int(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = 'invalid type'
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ 50
+ )
+ self.assertEqual(result, 50)
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_if_below_minimum_1(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = -5
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ default_value=50
+ )
+ self.assertEqual(result, 50)
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_if_below_minimum_2(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = -50
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ default_value=50,
+ min_value=-10
+ )
+ self.assertEqual(result, 50)
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_for_invalid_type_int(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = 1000
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ default_value=50,
+ max_value=100
+ )
+ self.assertEqual(result, 50)
+
+
+ @patch(MODULE_PATH + '.app_settings.settings')
+ def test_default_is_none_needs_required_type(self, mock_settings):
+ mock_settings.TEST_SETTING_DUMMY = 'invalid type'
+ with self.assertRaises(ValueError):
+ result = app_settings._clean_setting(
+ 'TEST_SETTING_DUMMY',
+ default_value=None
+ )
+
+
diff --git a/allianceauth/groupmanagement/admin.py b/allianceauth/groupmanagement/admin.py
index 2fc69ca7..eaf9060d 100644
--- a/allianceauth/groupmanagement/admin.py
+++ b/allianceauth/groupmanagement/admin.py
@@ -1,11 +1,24 @@
+from django.conf import settings
+
from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup
-from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
+from django.db.models import Count
+from django.db.models.functions import Lower
+from django.db.models.signals import pre_save, post_save, pre_delete, \
+ post_delete, m2m_changed
from django.dispatch import receiver
+
from .models import AuthGroup
from .models import GroupRequest
from . import signals
+if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
+ _has_auto_groups = True
+ from allianceauth.eveonline.autogroups.models import *
+else:
+ _has_auto_groups = False
+
+
class AuthGroupInlineAdmin(admin.StackedInline):
model = AuthGroup
filter_horizontal = ('group_leaders', 'group_leader_groups', 'states',)
@@ -13,6 +26,17 @@ class AuthGroupInlineAdmin(admin.StackedInline):
verbose_name_plural = 'Auth Settings'
verbose_name = ''
+ def formfield_for_manytomany(self, db_field, request, **kwargs):
+ """overriding this formfield to have sorted lists in the form"""
+ if db_field.name == "group_leaders":
+ kwargs["queryset"] = User.objects\
+ .filter(profile__state__name='Member')\
+ .order_by(Lower('username'))
+ elif db_field.name == "group_leader_groups":
+ kwargs["queryset"] = Group.objects\
+ .order_by(Lower('name'))
+ return super().formfield_for_manytomany(db_field, request, **kwargs)
+
def has_add_permission(self, request):
return False
@@ -23,7 +47,116 @@ class AuthGroupInlineAdmin(admin.StackedInline):
return request.user.has_perm('auth.change_group')
-class GroupAdmin(admin.ModelAdmin):
+if _has_auto_groups:
+ class IsAutoGroupFilter(admin.SimpleListFilter):
+ title = 'auto group'
+ parameter_name = 'is_auto_group__exact'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('yes', 'Yes'),
+ ('no', 'No'),
+ )
+
+ def queryset(self, request, queryset):
+ value = self.value()
+ if value == 'yes':
+ return queryset.exclude(
+ managedalliancegroup__isnull=True,
+ managedcorpgroup__isnull=True
+ )
+ elif value == 'no':
+ return queryset.filter(
+ managedalliancegroup__isnull=True,
+ managedcorpgroup__isnull=True
+ )
+ else:
+ return queryset
+
+
+class HasLeaderFilter(admin.SimpleListFilter):
+ title = 'has leader'
+ parameter_name = 'has_leader__exact'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('yes', 'Yes'),
+ ('no', 'No'),
+ )
+
+ def queryset(self, request, queryset):
+ value = self.value()
+ if value == 'yes':
+ return queryset.filter(authgroup__group_leaders__isnull=False)
+ elif value == 'no':
+ return queryset.filter(authgroup__group_leaders__isnull=True)
+ else:
+ return queryset
+
+class GroupAdmin(admin.ModelAdmin):
+ list_select_related = True
+ ordering = ('name', )
+ list_display = (
+ 'name',
+ '_description',
+ '_properties',
+ '_member_count',
+ 'has_leader'
+ )
+ list_filter = (
+ 'authgroup__internal',
+ 'authgroup__hidden',
+ 'authgroup__open',
+ 'authgroup__public',
+ IsAutoGroupFilter,
+ HasLeaderFilter
+ )
+ search_fields = ('name', 'authgroup__description')
+
+ def get_queryset(self, request):
+ qs = super().get_queryset(request)
+ qs = qs.annotate(
+ member_count=Count('user', distinct=True),
+ )
+ return qs
+
+ def _description(self, obj):
+ return obj.authgroup.description
+
+ def _member_count(self, obj):
+ return obj.member_count
+
+ _member_count.short_description = 'Members'
+ _member_count.admin_order_field = 'member_count'
+
+ def has_leader(self, obj):
+ return obj.authgroup.group_leaders.exists()
+
+ has_leader.boolean = True
+
+ def _properties(self, obj):
+ properties = list()
+ if _has_auto_groups and (
+ obj.managedalliancegroup_set.exists()
+ or obj.managedcorpgroup_set.exists()
+ ):
+ properties.append('Auto Group')
+ elif obj.authgroup.internal:
+ properties.append('Internal')
+ else:
+ if obj.authgroup.hidden:
+ properties.append('Hidden')
+ if obj.authgroup.open:
+ properties.append('Open')
+ if obj.authgroup.public:
+ properties.append('Public')
+ if not properties:
+ properties.append('Default')
+
+ return properties
+
+ _properties.short_description = "properties"
+
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
diff --git a/allianceauth/groupmanagement/tests/__init__.py b/allianceauth/groupmanagement/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/allianceauth/groupmanagement/tests/test_admin.py b/allianceauth/groupmanagement/tests/test_admin.py
new file mode 100644
index 00000000..2d9f452b
--- /dev/null
+++ b/allianceauth/groupmanagement/tests/test_admin.py
@@ -0,0 +1,379 @@
+from unittest.mock import patch
+
+from django.test import TestCase, RequestFactory
+from django.contrib import admin
+from django.contrib.admin.sites import AdminSite
+from django.contrib.auth.models import User
+
+from allianceauth.authentication.models import CharacterOwnership, State
+from allianceauth.eveonline.autogroups.models import AutogroupsConfig
+from allianceauth.eveonline.models import (
+ EveCharacter, EveCorporationInfo, EveAllianceInfo
+)
+
+from ..admin import (
+ IsAutoGroupFilter,
+ HasLeaderFilter,
+ GroupAdmin,
+ Group
+)
+
+
+MODULE_PATH = 'allianceauth.groupmanagement.admin'
+
+
+class MockRequest(object):
+
+ def __init__(self, user=None):
+ self.user = user
+
+
+class TestGroupAdmin(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # group 1 - has leader
+ cls.group_1 = Group.objects.create(name='Group 1')
+ cls.group_1.authgroup.description = 'Default Group'
+ cls.group_1.authgroup.internal = False
+ cls.group_1.authgroup.hidden = False
+ cls.group_1.authgroup.save()
+
+ # 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.save()
+
+ # group 3 - has leader
+ cls.group_3 = Group.objects.create(name='Group 3')
+ cls.group_3.authgroup.description = 'Hidden Group'
+ cls.group_3.authgroup.internal = False
+ cls.group_3.authgroup.hidden = True
+ cls.group_3.authgroup.save()
+
+ # group 4 - no leader
+ cls.group_4 = Group.objects.create(name='Group 4')
+ cls.group_4.authgroup.description = 'Open Group'
+ cls.group_4.authgroup.internal = False
+ cls.group_4.authgroup.hidden = False
+ cls.group_4.authgroup.open = True
+ cls.group_4.authgroup.save()
+
+ # group 5 - no leader
+ cls.group_5 = Group.objects.create(name='Group 5')
+ cls.group_5.authgroup.description = 'Public Group'
+ cls.group_5.authgroup.internal = False
+ cls.group_5.authgroup.hidden = False
+ cls.group_5.authgroup.public = True
+ cls.group_5.authgroup.save()
+
+ # group 6 - no leader
+ cls.group_6 = Group.objects.create(name='Group 6')
+ cls.group_6.authgroup.description = 'Mixed Group'
+ cls.group_6.authgroup.internal = False
+ cls.group_6.authgroup.hidden = True
+ cls.group_6.authgroup.open = True
+ cls.group_6.authgroup.public = True
+ cls.group_6.authgroup.save()
+
+ # user 1 - corp and alliance, normal user
+ cls.character_1 = EveCharacter.objects.create(
+ character_id='1001',
+ character_name='Bruce Wayne',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ cls.character_1a = EveCharacter.objects.create(
+ character_id='1002',
+ character_name='Batman',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ alliance = EveAllianceInfo.objects.create(
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ executor_corp_id='2001'
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ member_count=42,
+ alliance=alliance
+ )
+ cls.user_1 = User.objects.create_user(
+ cls.character_1.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1,
+ owner_hash='x1' + cls.character_1.character_name,
+ user=cls.user_1
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1a,
+ owner_hash='x1' + cls.character_1a.character_name,
+ user=cls.user_1
+ )
+ cls.user_1.profile.main_character = cls.character_1
+ cls.user_1.profile.save()
+ cls.user_1.groups.add(cls.group_1)
+ cls.group_1.authgroup.group_leaders.add(cls.user_1)
+
+ # user 2 - corp only, staff
+ cls.character_2 = EveCharacter.objects.create(
+ character_id=1003,
+ character_name='Clark Kent',
+ corporation_id=2002,
+ corporation_name='Daily Planet',
+ corporation_ticker='DP',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2002,
+ corporation_name='Daily Plane',
+ corporation_ticker='DP',
+ member_count=99,
+ alliance=None
+ )
+ cls.user_2 = User.objects.create_user(
+ cls.character_2.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_2,
+ owner_hash='x1' + cls.character_2.character_name,
+ user=cls.user_2
+ )
+ cls.user_2.profile.main_character = cls.character_2
+ cls.user_2.profile.save()
+ cls.user_2.groups.add(cls.group_2)
+ cls.user_2.is_staff = True
+ cls.user_2.save()
+
+ # user 3 - no main, no group, superuser
+ cls.character_3 = EveCharacter.objects.create(
+ character_id=1101,
+ character_name='Lex Luthor',
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ member_count=666,
+ alliance=None
+ )
+ EveAllianceInfo.objects.create(
+ alliance_id='3101',
+ alliance_name='Lex World Domination',
+ alliance_ticker='LWD',
+ executor_corp_id=''
+ )
+ cls.user_3 = User.objects.create_user(
+ cls.character_3.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_3,
+ owner_hash='x1' + cls.character_3.character_name,
+ user=cls.user_3
+ )
+ cls.user_3.is_superuser = True
+ cls.user_3.save()
+ cls.user_3.groups.add(cls.group_3)
+ cls.group_3.authgroup.group_leaders.add(cls.user_3)
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.modeladmin = GroupAdmin(
+ model=Group, admin_site=AdminSite()
+ )
+
+ def _create_autogroups(self):
+ """create autogroups for corps and alliances"""
+ 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_description(self):
+ expected = 'Default Group'
+ result = self.modeladmin._description(self.group_1)
+ self.assertEqual(result, expected)
+
+ def test_member_count(self):
+ expected = 1
+ obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\
+ .get(pk=self.group_1.pk)
+ result = self.modeladmin._member_count(obj)
+ self.assertEqual(result, expected)
+
+ def test_has_leader(self):
+ result = self.modeladmin.has_leader(self.group_1)
+ self.assertTrue(result)
+
+ def test_properties_1(self):
+ expected = ['Default']
+ result = self.modeladmin._properties(self.group_1)
+ self.assertListEqual(result, expected)
+
+ def test_properties_2(self):
+ expected = ['Internal']
+ result = self.modeladmin._properties(self.group_2)
+ self.assertListEqual(result, expected)
+
+ def test_properties_3(self):
+ expected = ['Hidden']
+ result = self.modeladmin._properties(self.group_3)
+ self.assertListEqual(result, expected)
+
+ def test_properties_4(self):
+ expected = ['Open']
+ result = self.modeladmin._properties(self.group_4)
+ self.assertListEqual(result, expected)
+
+ def test_properties_5(self):
+ expected = ['Public']
+ result = self.modeladmin._properties(self.group_5)
+ self.assertListEqual(result, expected)
+
+ def test_properties_6(self):
+ expected = ['Hidden', 'Open', 'Public']
+ result = self.modeladmin._properties(self.group_6)
+ self.assertListEqual(result, expected)
+
+ @patch(MODULE_PATH + '._has_auto_groups', True)
+ def test_properties_6(self):
+ self._create_autogroups()
+ expected = ['Auto Group']
+ my_group = Group.objects\
+ .filter(managedcorpgroup__isnull=False)\
+ .first()
+ result = self.modeladmin._properties(my_group)
+ self.assertListEqual(result, expected)
+
+ # actions
+
+ # filters
+
+ @patch(MODULE_PATH + '._has_auto_groups', True)
+ def test_filter_is_auto_group(self):
+
+ class GroupAdminTest(admin.ModelAdmin):
+ list_filter = (IsAutoGroupFilter,)
+
+ self._create_autogroups()
+ my_modeladmin = GroupAdminTest(Group, 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 = [
+ ('yes', 'Yes'),
+ ('no', 'No'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned - no
+ request = self.factory.get(
+ '/', {'is_auto_group__exact': 'no'}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [
+ self.group_1,
+ self.group_2,
+ self.group_3,
+ self.group_4,
+ self.group_5,
+ self.group_6
+ ]
+ self.assertSetEqual(set(queryset), set(expected))
+
+ # Make sure the correct queryset is returned - yes
+ request = self.factory.get(
+ '/', {'is_auto_group__exact': 'yes'}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = Group.objects.exclude(
+ managedalliancegroup__isnull=True,
+ managedcorpgroup__isnull=True
+ )
+ self.assertSetEqual(set(queryset), set(expected))
+
+ def test_filter_has_leader(self):
+
+ class GroupAdminTest(admin.ModelAdmin):
+ list_filter = (HasLeaderFilter,)
+
+ self._create_autogroups()
+ my_modeladmin = GroupAdminTest(Group, 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 = [
+ ('yes', 'Yes'),
+ ('no', 'No'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned - no
+ request = self.factory.get(
+ '/', {'has_leader__exact': 'no'}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = Group.objects.exclude(pk__in=[
+ self.group_1.pk, self.group_3.pk
+ ])
+ self.assertSetEqual(set(queryset), set(expected))
+
+ # Make sure the correct queryset is returned - yes
+ request = self.factory.get(
+ '/', {'has_leader__exact': 'yes'}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [
+ self.group_1,
+ self.group_3
+ ]
+ self.assertSetEqual(set(queryset), set(expected))
+
\ No newline at end of file
diff --git a/allianceauth/groupmanagement/tests.py b/allianceauth/groupmanagement/tests/test_all.py
similarity index 100%
rename from allianceauth/groupmanagement/tests.py
rename to allianceauth/groupmanagement/tests/test_all.py
diff --git a/allianceauth/services/admin.py b/allianceauth/services/admin.py
index d37a30fb..061b97e1 100644
--- a/allianceauth/services/admin.py
+++ b/allianceauth/services/admin.py
@@ -1,9 +1,50 @@
-from django.contrib import admin
from django import forms
+from django.contrib import admin
+from django.db.models.functions import Lower
+from django.urls import reverse
+from django.utils.html import format_html
+
from allianceauth import hooks
+from allianceauth.eveonline.models import EveCharacter
+from allianceauth.authentication.admin import user_profile_pic, \
+ user_username, user_main_organization, MainCorporationsFilter,\
+ MainAllianceFilter
+
from .models import NameFormatConfig
+class ServicesUserAdmin(admin.ModelAdmin):
+ """Parent class for UserAdmin classes for all services"""
+ class Media:
+ css = {
+ "all": ("services/admin.css",)
+ }
+
+ search_fields = (
+ 'user__username',
+ 'uid'
+ )
+ ordering = ('user__username', )
+ list_select_related = True
+ list_display = (
+ user_profile_pic,
+ user_username,
+ user_main_organization,
+ '_date_joined'
+ )
+ list_filter = (
+ MainCorporationsFilter,
+ MainAllianceFilter,
+ 'user__date_joined'
+ )
+
+ def _date_joined(self, obj):
+ return obj.user.date_joined
+
+ _date_joined.short_description = 'date joined'
+ _date_joined.admin_order_field = 'user__date_joined'
+
+
class NameFormatConfigForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(NameFormatConfigForm, self).__init__(*args, **kwargs)
diff --git a/allianceauth/services/modules/discord/admin.py b/allianceauth/services/modules/discord/admin.py
index 1f47945d..c5b12467 100644
--- a/allianceauth/services/modules/discord/admin.py
+++ b/allianceauth/services/modules/discord/admin.py
@@ -1,9 +1,18 @@
from django.contrib import admin
+
from .models import DiscordUser
+from ...admin import ServicesUserAdmin
-class DiscordUserAdmin(admin.ModelAdmin):
- list_display = ('user', 'uid')
- search_fields = ('user__username', 'uid')
+@admin.register(DiscordUser)
+class DiscordUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + (
+ '_uid',
+ )
+
+ def _uid(self, obj):
+ return obj.uid
+
+ _uid.short_description = 'Discord ID (UID)'
+ _uid.admin_order_field = 'uid'
-admin.site.register(DiscordUser, DiscordUserAdmin)
diff --git a/allianceauth/services/modules/discord/tests/__init__.py b/allianceauth/services/modules/discord/tests/__init__.py
new file mode 100644
index 00000000..3d5481cb
--- /dev/null
+++ b/allianceauth/services/modules/discord/tests/__init__.py
@@ -0,0 +1,10 @@
+from django.contrib.auth.models import User, Group, Permission
+from allianceauth.tests.auth_utils import AuthUtils
+
+DEFAULT_AUTH_GROUP = 'Member'
+MODULE_PATH = 'allianceauth.services.modules.discord'
+
+def add_permissions():
+ permission = Permission.objects.get(codename='access_discord')
+ members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
+ AuthUtils.add_permissions_to_groups([permission], [members])
diff --git a/allianceauth/services/modules/discord/tests/test_admin.py b/allianceauth/services/modules/discord/tests/test_admin.py
new file mode 100644
index 00000000..5d9c4dc1
--- /dev/null
+++ b/allianceauth/services/modules/discord/tests/test_admin.py
@@ -0,0 +1,268 @@
+from unittest.mock import patch
+
+from django.test import TestCase, RequestFactory
+from django.contrib import admin
+from django.contrib.admin.sites import AdminSite
+from django.contrib.auth.models import User
+
+from allianceauth.authentication.models import CharacterOwnership
+from allianceauth.eveonline.models import (
+ EveCharacter, EveCorporationInfo, EveAllianceInfo
+)
+
+from ....admin import (
+ user_profile_pic,
+ user_username,
+ user_main_organization,
+ ServicesUserAdmin,
+ MainCorporationsFilter,
+ MainAllianceFilter
+)
+from ..admin import (
+ DiscordUser,
+ DiscordUserAdmin
+)
+
+
+class TestDiscordUserAdmin(TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # user 1 - corp and alliance, normal user
+ cls.character_1 = EveCharacter.objects.create(
+ character_id='1001',
+ character_name='Bruce Wayne',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ cls.character_1a = EveCharacter.objects.create(
+ character_id='1002',
+ character_name='Batman',
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ )
+ alliance = EveAllianceInfo.objects.create(
+ alliance_id='3001',
+ alliance_name='Wayne Enterprises',
+ alliance_ticker='WE',
+ executor_corp_id='2001'
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id='2001',
+ corporation_name='Wayne Technologies',
+ corporation_ticker='WT',
+ member_count=42,
+ alliance=alliance
+ )
+ cls.user_1 = User.objects.create_user(
+ cls.character_1.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1,
+ owner_hash='x1' + cls.character_1.character_name,
+ user=cls.user_1
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_1a,
+ owner_hash='x1' + cls.character_1a.character_name,
+ user=cls.user_1
+ )
+ cls.user_1.profile.main_character = cls.character_1
+ cls.user_1.profile.save()
+ DiscordUser.objects.create(
+ user=cls.user_1,
+ uid=1001
+ )
+
+ # user 2 - corp only, staff
+ cls.character_2 = EveCharacter.objects.create(
+ character_id=1003,
+ character_name='Clark Kent',
+ corporation_id=2002,
+ corporation_name='Daily Planet',
+ corporation_ticker='DP',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2002,
+ corporation_name='Daily Plane',
+ corporation_ticker='DP',
+ member_count=99,
+ alliance=None
+ )
+ cls.user_2 = User.objects.create_user(
+ cls.character_2.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_2,
+ owner_hash='x1' + cls.character_2.character_name,
+ user=cls.user_2
+ )
+ cls.user_2.profile.main_character = cls.character_2
+ cls.user_2.profile.save()
+ DiscordUser.objects.create(
+ user=cls.user_2,
+ uid=1002
+ )
+
+ # user 3 - no main, no group, superuser
+ cls.character_3 = EveCharacter.objects.create(
+ character_id=1101,
+ character_name='Lex Luthor',
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ alliance_id=None
+ )
+ EveCorporationInfo.objects.create(
+ corporation_id=2101,
+ corporation_name='Lex Corp',
+ corporation_ticker='LC',
+ member_count=666,
+ alliance=None
+ )
+ EveAllianceInfo.objects.create(
+ alliance_id='3101',
+ alliance_name='Lex World Domination',
+ alliance_ticker='LWD',
+ executor_corp_id=''
+ )
+ cls.user_3 = User.objects.create_user(
+ cls.character_3.character_name.replace(' ', '_'),
+ 'abc@example.com',
+ 'password'
+ )
+ CharacterOwnership.objects.create(
+ character=cls.character_3,
+ owner_hash='x1' + cls.character_3.character_name,
+ user=cls.user_3
+ )
+ DiscordUser.objects.create(
+ user=cls.user_3,
+ uid=1003
+ )
+
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.modeladmin = DiscordUserAdmin(
+ model=DiscordUser, admin_site=AdminSite()
+ )
+
+ # column rendering
+
+ def test_user_profile_pic_u1(self):
+ expected = ('
')
+ self.assertEqual(user_profile_pic(self.user_1.discord), expected)
+
+ def test_user_profile_pic_u3(self):
+ self.assertIsNone(user_profile_pic(self.user_3.discord))
+
+ def test_user_username_u1(self):
+ expected = (
+ ''
+ 'Bruce_Wayne
Bruce Wayne'.format(
+ self.user_1.discord.pk
+ )
+ )
+ self.assertEqual(user_username(self.user_1.discord), expected)
+
+ def test_user_username_u3(self):
+ expected = (
+ ''
+ 'Lex_Luthor'.format(self.user_3.discord.pk)
+ )
+ self.assertEqual(user_username(self.user_3.discord), expected)
+
+ def test_user_main_organization_u1(self):
+ expected = 'Wayne Technologies
Wayne Enterprises'
+ result = user_main_organization(self.user_1.discord)
+ self.assertEqual(result, expected)
+
+ def test_user_main_organization_u2(self):
+ expected = 'Daily Planet'
+ result = user_main_organization(self.user_2.discord)
+ self.assertEqual(result, expected)
+
+ def test_user_main_organization_u3(self):
+ expected = None
+ result = user_main_organization(self.user_3.discord)
+ self.assertEqual(result, expected)
+
+ # actions
+
+ # filters
+ def test_filter_main_corporations(self):
+
+ class DiscordUserAdminTest(ServicesUserAdmin):
+ list_filter = (MainCorporationsFilter,)
+
+ my_modeladmin = DiscordUserAdminTest(DiscordUser, 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 = [
+ ('2002', 'Daily Planet'),
+ ('2001', 'Wayne Technologies'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned
+ request = self.factory.get(
+ '/',
+ {'main_corporation_id__exact': self.character_1.corporation_id}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [self.user_1.discord]
+ self.assertSetEqual(set(queryset), set(expected))
+
+ def test_filter_main_alliances(self):
+
+ class DiscordUserAdminTest(ServicesUserAdmin):
+ list_filter = (MainAllianceFilter,)
+
+ my_modeladmin = DiscordUserAdminTest(DiscordUser, 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 = [
+ ('3001', 'Wayne Enterprises'),
+ ]
+ self.assertEqual(filterspec.lookup_choices, expected)
+
+ # Make sure the correct queryset is returned
+ request = self.factory.get(
+ '/',
+ {'main_alliance_id__exact': self.character_1.alliance_id}
+ )
+ request.user = self.user_1
+ changelist = my_modeladmin.get_changelist_instance(request)
+ queryset = changelist.get_queryset(request)
+ expected = [self.user_1.discord]
+ self.assertSetEqual(set(queryset), set(expected))
+
\ No newline at end of file
diff --git a/allianceauth/services/modules/discord/tests/test_hooks.py b/allianceauth/services/modules/discord/tests/test_hooks.py
new file mode 100644
index 00000000..d7c82c8c
--- /dev/null
+++ b/allianceauth/services/modules/discord/tests/test_hooks.py
@@ -0,0 +1,127 @@
+from unittest import mock
+
+from django.test import TestCase, RequestFactory
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from allianceauth.tests.auth_utils import AuthUtils
+
+from ..auth_hooks import DiscordService
+from ..models import DiscordUser
+from ..tasks import DiscordTasks
+from ..manager import DiscordOAuthManager
+
+from . import DEFAULT_AUTH_GROUP, add_permissions, MODULE_PATH
+
+
+class DiscordHooksTestCase(TestCase):
+ def setUp(self):
+ self.member = 'member_user'
+ member = AuthUtils.create_member(self.member)
+ DiscordUser.objects.create(user=member, uid='12345')
+ self.none_user = 'none_user'
+ none_user = AuthUtils.create_user(self.none_user)
+ self.service = DiscordService
+ add_permissions()
+
+ def test_has_account(self):
+ member = User.objects.get(username=self.member)
+ none_user = User.objects.get(username=self.none_user)
+ self.assertTrue(DiscordTasks.has_account(member))
+ self.assertFalse(DiscordTasks.has_account(none_user))
+
+ def test_service_enabled(self):
+ service = self.service()
+ member = User.objects.get(username=self.member)
+ none_user = User.objects.get(username=self.none_user)
+
+ self.assertTrue(service.service_active_for_user(member))
+ self.assertFalse(service.service_active_for_user(none_user))
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_update_all_groups(self, manager):
+ service = self.service()
+ service.update_all_groups()
+ # Check member and blue user have groups updated
+ self.assertTrue(manager.update_groups.called)
+ self.assertEqual(manager.update_groups.call_count, 1)
+
+ def test_update_groups(self):
+ # Check member has Member group updated
+ with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager:
+ service = self.service()
+ member = User.objects.get(username=self.member)
+ AuthUtils.disconnect_signals()
+ service.update_groups(member)
+ self.assertTrue(manager.update_groups.called)
+ args, kwargs = manager.update_groups.call_args
+ user_id, groups = args
+ self.assertIn(DEFAULT_AUTH_GROUP, groups)
+ self.assertEqual(user_id, member.discord.uid)
+
+ # Check none user does not have groups updated
+ with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager:
+ service = self.service()
+ none_user = User.objects.get(username=self.none_user)
+ service.update_groups(none_user)
+ self.assertFalse(manager.update_groups.called)
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_validate_user(self, manager):
+ service = self.service()
+ # Test member is not deleted
+ member = User.objects.get(username=self.member)
+ service.validate_user(member)
+ self.assertTrue(member.discord)
+
+ # Test none user is deleted
+ none_user = User.objects.get(username=self.none_user)
+ DiscordUser.objects.create(user=none_user, uid='abc123')
+ service.validate_user(none_user)
+ self.assertTrue(manager.delete_user.called)
+ with self.assertRaises(ObjectDoesNotExist):
+ none_discord = User.objects.get(username=self.none_user).discord
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_sync_nickname(self, manager):
+ service = self.service()
+ member = User.objects.get(username=self.member)
+ AuthUtils.add_main_character(member, 'test user', '12345', corp_ticker='AAUTH')
+
+ service.sync_nickname(member)
+
+ self.assertTrue(manager.update_nickname.called)
+ args, kwargs = manager.update_nickname.call_args
+ self.assertEqual(args[0], member.discord.uid)
+ self.assertEqual(args[1], 'test user')
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_delete_user(self, manager):
+ member = User.objects.get(username=self.member)
+
+ service = self.service()
+ result = service.delete_user(member)
+
+ self.assertTrue(result)
+ self.assertTrue(manager.delete_user.called)
+ with self.assertRaises(ObjectDoesNotExist):
+ discord_user = User.objects.get(username=self.member).discord
+
+ def test_render_services_ctrl(self):
+ service = self.service()
+ member = User.objects.get(username=self.member)
+ request = RequestFactory().get('/services/')
+ request.user = member
+
+ response = service.render_services_ctrl(request)
+ self.assertTemplateUsed(service.service_ctrl_template)
+ self.assertIn('/discord/reset/', response)
+ self.assertIn('/discord/deactivate/', response)
+
+ # Test register becomes available
+ member.discord.delete()
+ member = User.objects.get(username=self.member)
+ request.user = member
+ response = service.render_services_ctrl(request)
+ self.assertIn('/discord/activate/', response)
+
+ # TODO: Test update nicknames
diff --git a/allianceauth/services/modules/discord/tests.py b/allianceauth/services/modules/discord/tests/test_managers.py
similarity index 55%
rename from allianceauth/services/modules/discord/tests.py
rename to allianceauth/services/modules/discord/tests/test_managers.py
index 87916fd0..2ee838a7 100644
--- a/allianceauth/services/modules/discord/tests.py
+++ b/allianceauth/services/modules/discord/tests/test_managers.py
@@ -2,197 +2,15 @@ import json
import urllib
import datetime
import requests_mock
-from django_webtest import WebTest
from unittest import mock
-from django.test import TestCase, RequestFactory
-from django.contrib.auth.models import User, Group, Permission
-from django.core.exceptions import ObjectDoesNotExist
+from django.test import TestCase
from django.conf import settings
-from allianceauth.tests.auth_utils import AuthUtils
-from .auth_hooks import DiscordService
-from .models import DiscordUser
-from .tasks import DiscordTasks
-from .manager import DiscordOAuthManager
-from . import manager
+from ..manager import DiscordOAuthManager
+from .. import manager
-
-MODULE_PATH = 'allianceauth.services.modules.discord'
-DEFAULT_AUTH_GROUP = 'Member'
-
-
-def add_permissions():
- permission = Permission.objects.get(codename='access_discord')
- members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
- AuthUtils.add_permissions_to_groups([permission], [members])
-
-
-class DiscordHooksTestCase(TestCase):
- def setUp(self):
- self.member = 'member_user'
- member = AuthUtils.create_member(self.member)
- DiscordUser.objects.create(user=member, uid='12345')
- self.none_user = 'none_user'
- none_user = AuthUtils.create_user(self.none_user)
- self.service = DiscordService
- add_permissions()
-
- def test_has_account(self):
- member = User.objects.get(username=self.member)
- none_user = User.objects.get(username=self.none_user)
- self.assertTrue(DiscordTasks.has_account(member))
- self.assertFalse(DiscordTasks.has_account(none_user))
-
- def test_service_enabled(self):
- service = self.service()
- member = User.objects.get(username=self.member)
- none_user = User.objects.get(username=self.none_user)
-
- self.assertTrue(service.service_active_for_user(member))
- self.assertFalse(service.service_active_for_user(none_user))
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_update_all_groups(self, manager):
- service = self.service()
- service.update_all_groups()
- # Check member and blue user have groups updated
- self.assertTrue(manager.update_groups.called)
- self.assertEqual(manager.update_groups.call_count, 1)
-
- def test_update_groups(self):
- # Check member has Member group updated
- with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager:
- service = self.service()
- member = User.objects.get(username=self.member)
- AuthUtils.disconnect_signals()
- service.update_groups(member)
- self.assertTrue(manager.update_groups.called)
- args, kwargs = manager.update_groups.call_args
- user_id, groups = args
- self.assertIn(DEFAULT_AUTH_GROUP, groups)
- self.assertEqual(user_id, member.discord.uid)
-
- # Check none user does not have groups updated
- with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager:
- service = self.service()
- none_user = User.objects.get(username=self.none_user)
- service.update_groups(none_user)
- self.assertFalse(manager.update_groups.called)
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_validate_user(self, manager):
- service = self.service()
- # Test member is not deleted
- member = User.objects.get(username=self.member)
- service.validate_user(member)
- self.assertTrue(member.discord)
-
- # Test none user is deleted
- none_user = User.objects.get(username=self.none_user)
- DiscordUser.objects.create(user=none_user, uid='abc123')
- service.validate_user(none_user)
- self.assertTrue(manager.delete_user.called)
- with self.assertRaises(ObjectDoesNotExist):
- none_discord = User.objects.get(username=self.none_user).discord
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_sync_nickname(self, manager):
- service = self.service()
- member = User.objects.get(username=self.member)
- AuthUtils.add_main_character(member, 'test user', '12345', corp_ticker='AAUTH')
-
- service.sync_nickname(member)
-
- self.assertTrue(manager.update_nickname.called)
- args, kwargs = manager.update_nickname.call_args
- self.assertEqual(args[0], member.discord.uid)
- self.assertEqual(args[1], 'test user')
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_delete_user(self, manager):
- member = User.objects.get(username=self.member)
-
- service = self.service()
- result = service.delete_user(member)
-
- self.assertTrue(result)
- self.assertTrue(manager.delete_user.called)
- with self.assertRaises(ObjectDoesNotExist):
- discord_user = User.objects.get(username=self.member).discord
-
- def test_render_services_ctrl(self):
- service = self.service()
- member = User.objects.get(username=self.member)
- request = RequestFactory().get('/services/')
- request.user = member
-
- response = service.render_services_ctrl(request)
- self.assertTemplateUsed(service.service_ctrl_template)
- self.assertIn('/discord/reset/', response)
- self.assertIn('/discord/deactivate/', response)
-
- # Test register becomes available
- member.discord.delete()
- member = User.objects.get(username=self.member)
- request.user = member
- response = service.render_services_ctrl(request)
- self.assertIn('/discord/activate/', response)
-
- # TODO: Test update nicknames
-
-
-class DiscordViewsTestCase(WebTest):
- def setUp(self):
- self.member = AuthUtils.create_member('auth_member')
- AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
- add_permissions()
-
- def login(self):
- self.app.set_user(self.member)
-
- @mock.patch(MODULE_PATH + '.views.DiscordOAuthManager')
- def test_activate(self, manager):
- self.login()
- manager.generate_oauth_redirect_url.return_value = '/example.com/oauth/'
- response = self.app.get('/discord/activate/', auto_follow=False)
- self.assertRedirects(response, expected_url='/example.com/oauth/', target_status_code=404)
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_callback(self, manager):
- self.login()
- manager.add_user.return_value = '1234'
- response = self.app.get('/discord/callback/', params={'code': '1234'})
-
- self.member = User.objects.get(pk=self.member.pk)
-
- self.assertTrue(manager.add_user.called)
- self.assertEqual(manager.update_nickname.called, settings.DISCORD_SYNC_NAMES)
- self.assertEqual(self.member.discord.uid, '1234')
- self.assertRedirects(response, expected_url='/services/', target_status_code=200)
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_reset(self, manager):
- self.login()
- DiscordUser.objects.create(user=self.member, uid='12345')
- manager.delete_user.return_value = True
-
- response = self.app.get('/discord/reset/')
-
- self.assertRedirects(response, expected_url='/discord/activate/', target_status_code=302)
-
- @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
- def test_deactivate(self, manager):
- self.login()
- DiscordUser.objects.create(user=self.member, uid='12345')
- manager.delete_user.return_value = True
-
- response = self.app.get('/discord/deactivate/')
-
- self.assertTrue(manager.delete_user.called)
- self.assertRedirects(response, expected_url='/services/', target_status_code=200)
- with self.assertRaises(ObjectDoesNotExist):
- discord_user = User.objects.get(pk=self.member.pk).discord
+from . import DEFAULT_AUTH_GROUP, add_permissions, MODULE_PATH
class DiscordManagerTestCase(TestCase):
diff --git a/allianceauth/services/modules/discord/tests/test_views.py b/allianceauth/services/modules/discord/tests/test_views.py
new file mode 100644
index 00000000..8de0e2eb
--- /dev/null
+++ b/allianceauth/services/modules/discord/tests/test_views.py
@@ -0,0 +1,66 @@
+from django_webtest import WebTest
+from unittest import mock
+
+from django.test import TestCase
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django.conf import settings
+from allianceauth.tests.auth_utils import AuthUtils
+
+from ..models import DiscordUser
+from ..manager import DiscordOAuthManager
+
+from . import DEFAULT_AUTH_GROUP, add_permissions, MODULE_PATH
+
+
+class DiscordViewsTestCase(WebTest):
+ def setUp(self):
+ self.member = AuthUtils.create_member('auth_member')
+ AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
+ add_permissions()
+
+ def login(self):
+ self.app.set_user(self.member)
+
+ @mock.patch(MODULE_PATH + '.views.DiscordOAuthManager')
+ def test_activate(self, manager):
+ self.login()
+ manager.generate_oauth_redirect_url.return_value = '/example.com/oauth/'
+ response = self.app.get('/discord/activate/', auto_follow=False)
+ self.assertRedirects(response, expected_url='/example.com/oauth/', target_status_code=404)
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_callback(self, manager):
+ self.login()
+ manager.add_user.return_value = '1234'
+ response = self.app.get('/discord/callback/', params={'code': '1234'})
+
+ self.member = User.objects.get(pk=self.member.pk)
+
+ self.assertTrue(manager.add_user.called)
+ self.assertEqual(manager.update_nickname.called, settings.DISCORD_SYNC_NAMES)
+ self.assertEqual(self.member.discord.uid, '1234')
+ self.assertRedirects(response, expected_url='/services/', target_status_code=200)
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_reset(self, manager):
+ self.login()
+ DiscordUser.objects.create(user=self.member, uid='12345')
+ manager.delete_user.return_value = True
+
+ response = self.app.get('/discord/reset/')
+
+ self.assertRedirects(response, expected_url='/discord/activate/', target_status_code=302)
+
+ @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
+ def test_deactivate(self, manager):
+ self.login()
+ DiscordUser.objects.create(user=self.member, uid='12345')
+ manager.delete_user.return_value = True
+
+ response = self.app.get('/discord/deactivate/')
+
+ self.assertTrue(manager.delete_user.called)
+ self.assertRedirects(response, expected_url='/services/', target_status_code=200)
+ with self.assertRaises(ObjectDoesNotExist):
+ discord_user = User.objects.get(pk=self.member.pk).discord
diff --git a/allianceauth/services/modules/discourse/admin.py b/allianceauth/services/modules/discourse/admin.py
index a6ad60c7..2e1abe26 100644
--- a/allianceauth/services/modules/discourse/admin.py
+++ b/allianceauth/services/modules/discourse/admin.py
@@ -1,9 +1,11 @@
from django.contrib import admin
+
from .models import DiscourseUser
+from ...admin import ServicesUserAdmin
-class DiscourseUserAdmin(admin.ModelAdmin):
- list_display = ('user',)
- search_fields = ('user__username',)
-
-admin.site.register(DiscourseUser, DiscourseUserAdmin)
+@admin.register(DiscourseUser)
+class DiscourseUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + (
+ 'enabled',
+ )
diff --git a/allianceauth/services/modules/mumble/admin.py b/allianceauth/services/modules/mumble/admin.py
index 387f7549..153772ae 100644
--- a/allianceauth/services/modules/mumble/admin.py
+++ b/allianceauth/services/modules/mumble/admin.py
@@ -1,10 +1,18 @@
from django.contrib import admin
+
from .models import MumbleUser
+from ...admin import ServicesUserAdmin
-class MumbleUserAdmin(admin.ModelAdmin):
- fields = ('user', 'username', 'groups') # pwhash is hidden from admin panel
- list_display = ('user', 'username', 'groups')
- search_fields = ('user__username', 'username', 'groups')
+@admin.register(MumbleUser)
+class MumbleUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + (
+ 'username',
+ 'groups',
+ )
+ search_fields = ServicesUserAdmin.search_fields + (
+ 'username',
+ 'groups'
+ )
-admin.site.register(MumbleUser, MumbleUserAdmin)
+ fields = ('user', 'username', 'groups') # pwhash is hidden from admin panel
diff --git a/allianceauth/services/modules/openfire/admin.py b/allianceauth/services/modules/openfire/admin.py
index d2564634..c0a1a9c7 100644
--- a/allianceauth/services/modules/openfire/admin.py
+++ b/allianceauth/services/modules/openfire/admin.py
@@ -1,9 +1,11 @@
from django.contrib import admin
+
from .models import OpenfireUser
+from ...admin import ServicesUserAdmin
-class OpenfireUserAdmin(admin.ModelAdmin):
- list_display = ('user', 'username')
- search_fields = ('user__username', 'username')
+@admin.register(OpenfireUser)
+class OpenfireUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + ('username',)
+ search_fields = ServicesUserAdmin.search_fields + ('username', )
-admin.site.register(OpenfireUser, OpenfireUserAdmin)
diff --git a/allianceauth/services/modules/phpbb3/admin.py b/allianceauth/services/modules/phpbb3/admin.py
index 210c9042..70e7f831 100644
--- a/allianceauth/services/modules/phpbb3/admin.py
+++ b/allianceauth/services/modules/phpbb3/admin.py
@@ -1,9 +1,9 @@
from django.contrib import admin
from .models import Phpbb3User
+from ...admin import ServicesUserAdmin
-class Phpbb3UserAdmin(admin.ModelAdmin):
- list_display = ('user', 'username')
- search_fields = ('user__username', 'username')
-
-admin.site.register(Phpbb3User, Phpbb3UserAdmin)
+@admin.register(Phpbb3User)
+class Phpbb3UserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + ('username',)
+ search_fields = ServicesUserAdmin.search_fields + ('username', )
\ No newline at end of file
diff --git a/allianceauth/services/modules/smf/admin.py b/allianceauth/services/modules/smf/admin.py
index 6afdca04..99c7f228 100644
--- a/allianceauth/services/modules/smf/admin.py
+++ b/allianceauth/services/modules/smf/admin.py
@@ -1,9 +1,10 @@
from django.contrib import admin
+
from .models import SmfUser
+from ...admin import ServicesUserAdmin
-class SmfUserAdmin(admin.ModelAdmin):
- list_display = ('user', 'username')
- search_fields = ('user__username', 'username')
-
-admin.site.register(SmfUser, SmfUserAdmin)
+@admin.register(SmfUser)
+class SmfUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + ('username',)
+ search_fields = ServicesUserAdmin.search_fields + ('username', )
\ No newline at end of file
diff --git a/allianceauth/services/modules/teamspeak3/admin.py b/allianceauth/services/modules/teamspeak3/admin.py
index ffde6642..0a6379dd 100644
--- a/allianceauth/services/modules/teamspeak3/admin.py
+++ b/allianceauth/services/modules/teamspeak3/admin.py
@@ -1,22 +1,36 @@
from django.contrib import admin
+
from .models import AuthTS, Teamspeak3User, StateGroup
+from ...admin import ServicesUserAdmin
-class Teamspeak3UserAdmin(admin.ModelAdmin):
- list_display = ('user', 'uid', 'perm_key')
- search_fields = ('user__username', 'uid', 'perm_key')
-
+@admin.register(Teamspeak3User)
+class Teamspeak3UserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + (
+ 'uid',
+ 'perm_key'
+ )
+
+@admin.register(AuthTS)
class AuthTSgroupAdmin(admin.ModelAdmin):
- fields = ['auth_group', 'ts_group']
+ ordering = ('auth_group__name', )
+ list_select_related = True
+
+ list_display = ('auth_group', '_ts_group')
+ list_filter = ('ts_group', )
+
+ fields = ('auth_group', 'ts_group')
filter_horizontal = ('ts_group',)
+ def _ts_group(self, obj):
+ 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'
+
@admin.register(StateGroup)
class StateGroupAdmin(admin.ModelAdmin):
list_display = ('state', 'ts_group')
search_fields = ('state__name', 'ts_group__ts_group_name')
-
-
-admin.site.register(AuthTS, AuthTSgroupAdmin)
-admin.site.register(Teamspeak3User, Teamspeak3UserAdmin)
diff --git a/allianceauth/services/modules/xenforo/admin.py b/allianceauth/services/modules/xenforo/admin.py
index 21321501..4da19bf2 100644
--- a/allianceauth/services/modules/xenforo/admin.py
+++ b/allianceauth/services/modules/xenforo/admin.py
@@ -1,9 +1,10 @@
from django.contrib import admin
+
from .models import XenforoUser
+from ...admin import ServicesUserAdmin
-class XenforoUserAdmin(admin.ModelAdmin):
- list_display = ('user', 'username')
- search_fields = ('user__username', 'username')
-
-admin.site.register(XenforoUser, XenforoUserAdmin)
+@admin.register(XenforoUser)
+class XenforoUserAdmin(ServicesUserAdmin):
+ list_display = ServicesUserAdmin.list_display + ('username',)
+ search_fields = ServicesUserAdmin.search_fields + ('username', )
\ No newline at end of file
diff --git a/allianceauth/services/static/services/admin.css b/allianceauth/services/static/services/admin.css
new file mode 100644
index 00000000..46796c5f
--- /dev/null
+++ b/allianceauth/services/static/services/admin.css
@@ -0,0 +1,6 @@
+/*
+CSS for allianceauth admin site
+*/
+
+.img-circle { border-radius: 50%; }
+.column-user_profile_pic { width: 50px; }
\ No newline at end of file