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