Compare commits

...

76 Commits

Author SHA1 Message Date
Ariel Rin
bb15de6d1a Version Bump v2.8.7 2021-09-14 04:32:37 +00:00
Ariel Rin
0f0c0441a9 Merge branch 'fix_logging_notifications' into 'master'
Fix logging notifications

See merge request allianceauth/allianceauth!1332
2021-08-20 04:03:48 +00:00
Erik Kalkoken
a0db8e8e2c Fix logging notifications 2021-08-20 04:03:48 +00:00
Ariel Rin
a7f468efd1 Merge branch 'fix_group_creation' into 'master'
Fix Group Creation

Closes #1161

See merge request allianceauth/allianceauth!1327
2021-08-11 06:07:24 +00:00
Ariel Rin
9f4ab9540b Merge branch 'improve_notifications' into 'master'
Improve notifications

See merge request allianceauth/allianceauth!1324
2021-08-11 06:06:24 +00:00
Erik Kalkoken
1e133b7c5d Improve notifications 2021-08-11 06:06:23 +00:00
Ariel Rin
4aa7530bbc Merge branch 'fix_hasgroupleader' into 'master'
Fix `Has Leader` column for groups that have group leader groups.

See merge request allianceauth/allianceauth!1328
2021-08-11 06:06:13 +00:00
colcrunch
2e0ddf2e7a has_leader should return true when a group has a group_leader_group 2021-07-13 18:00:21 -04:00
colcrunch
e24bc2a05d Revert "Update tests."
This reverts commit 7c1d1074f9.
2021-07-13 11:35:19 -04:00
colcrunch
a8c0db3fd7 Revert "Update autogroups."
This reverts commit eaa1cde01a.
2021-07-13 11:35:06 -04:00
colcrunch
7b77a6cd40 Revert "Add authutil for creating groups with authgroups."
This reverts commit 15db817382.
2021-07-13 11:34:55 -04:00
colcrunch
b8b8e470f2 Revert "Update tests to use the create_group util."
This reverts commit 0897383e41.
2021-07-13 11:34:46 -04:00
colcrunch
ad92ea243d Revert "Fix missed test."
This reverts commit 37005b1c68.
2021-07-13 11:34:35 -04:00
colcrunch
489a8456f7 Revert "More test fixes."
This reverts commit 6c3650d9f2.
2021-07-13 11:34:19 -04:00
colcrunch
122e389c38 Add signals back. 2021-07-13 11:33:51 -04:00
colcrunch
8318add6d5 Update test_admin.py 2021-07-13 10:18:39 -04:00
colcrunch
6c3650d9f2 More test fixes. 2021-07-13 09:28:31 -04:00
colcrunch
37005b1c68 Fix missed test. 2021-07-13 09:18:53 -04:00
colcrunch
0897383e41 Update tests to use the create_group util. 2021-07-13 09:13:47 -04:00
colcrunch
15db817382 Add authutil for creating groups with authgroups. 2021-07-13 09:13:17 -04:00
colcrunch
eaa1cde01a Update autogroups. 2021-07-13 08:41:36 -04:00
colcrunch
7c1d1074f9 Update tests. 2021-07-13 08:41:25 -04:00
colcrunch
0f0f9b6062 Fix group creation ignoring AuthGroup settings. (Fixes #1161) 2021-07-13 08:09:40 -04:00
Ariel Rin
6f2807cba2 Version Bump v2.8.6 2021-07-02 16:47:06 +00:00
Ariel Rin
39a40a8c43 Limit Django-Celery-Beat to 2.2.0 for Celery 4.x 2021-07-02 16:42:07 +00:00
Ariel Rin
5f98b5350e Version Bump v2.8.5 2021-06-29 03:27:30 +00:00
Ariel Rin
9de4d557e3 Merge branch 'discord-existing' into 'master'
Update member when they are already in Discord server.

See merge request allianceauth/allianceauth!1322
2021-06-29 03:10:23 +00:00
Aaron Kable
1d5f2634f1 Update member when they are already in Discord server. 2021-06-29 03:10:23 +00:00
Ariel Rin
710d26d36d Merge branch 'message-icons-fix' into 'master'
[FIX] icons in alert messages

See merge request allianceauth/allianceauth!1323
2021-06-29 03:10:01 +00:00
Peter Pfeufer
47793e6198 [FIX] icons in alert messages
This way they don't interfere with possible HTML in the message itself.
2021-06-28 21:20:59 +02:00
Ariel Rin
5fcb56a087 Merge branch 'notification_with_icons' into 'master'
Improve messages

See merge request allianceauth/allianceauth!1320
2021-06-21 10:39:56 +00:00
Erik Kalkoken
808080d5ee Improve messages 2021-06-21 10:39:56 +00:00
Ariel Rin
e6037f1680 Typo (Allince<Alliance) Credit @Thalimet 2021-06-21 10:23:40 +00:00
Ariel Rin
5c3ded6b07 Merge branch 'admin_performance_tuning' into 'master'
Admin performance tuning for group and state

See merge request allianceauth/allianceauth!1318
2021-06-01 11:40:32 +00:00
Erik Kalkoken
0c14e35d15 Admin performance tuning for group and state 2021-06-01 11:40:32 +00:00
Ariel Rin
c13be5d39a Merge branch 'ts3_update_button' into 'master'
Add button to update TS3 groups to admin page

See merge request allianceauth/allianceauth!1313
2021-06-01 11:38:24 +00:00
Erik Kalkoken
e4b515c1d5 Add button to update TS3 groups to admin page 2021-06-01 11:38:24 +00:00
Ariel Rin
65e2c87e8f Merge branch 'improve_eveonline_manager' into 'master'
Improve performance in EveCharacter manager

See merge request allianceauth/allianceauth!1314
2021-06-01 11:36:29 +00:00
Ariel Rin
68ce25854a Merge branch 'quick-syntax-fix-in-docu' into 'master'
quick fix in documentation

See merge request allianceauth/allianceauth!1317
2021-06-01 11:35:20 +00:00
Peter Pfeufer
3df6643513 this is a shell command ... 2021-05-17 17:41:28 +02:00
Ariel Rin
6ea0ebc9f9 Merge branch 'db-install-docs-update' into 'master'
Added a quick hint that it's ok to leave the SQL shell, and some commas

See merge request allianceauth/allianceauth!1315
2021-05-16 02:38:43 +00:00
Peter Pfeufer
26236f5886 Added a quick hint that it's ok to leave the SQL shell, and some commas 2021-05-15 15:27:50 +02:00
ErikKalkoken
1420c71ec5 Improve get_character_by_id() 2021-05-15 13:58:23 +02:00
Ariel Rin
5a2c9243c4 Version Bump v2.8.4 2021-05-09 05:52:20 +00:00
Ariel Rin
fecd748198 Merge branch 'mumble-version' into 'master'
Model updates for Mumble Authenticator 1.1

See merge request allianceauth/allianceauth!1303
2021-05-09 05:41:05 +00:00
Ariel Rin
85351b2c66 Model updates for Mumble Authenticator 1.1 2021-05-09 05:41:05 +00:00
Ariel Rin
8b3e5b6462 Merge branch 'improve_eveonline_admin' into 'master'
Improve admin pages for eveonline models

See merge request allianceauth/allianceauth!1306
2021-05-09 05:39:46 +00:00
Erik Kalkoken
93af920b8f Improve admin pages for eveonline models 2021-05-09 05:39:46 +00:00
Ariel Rin
b1248d9845 Merge branch 'improve_groups_admin' into 'master'
Improve group management admin pages

See merge request allianceauth/allianceauth!1308
2021-05-09 05:38:28 +00:00
Erik Kalkoken
c86abef07d Improve group management admin pages 2021-05-09 05:38:28 +00:00
Ariel Rin
17ad5dfe33 Merge branch 'improve_authentication_admin' into 'master'
Improve authentication admin

See merge request allianceauth/allianceauth!1307
2021-05-09 05:31:45 +00:00
Erik Kalkoken
e1843fe1f1 Improve authentication admin 2021-05-09 05:31:45 +00:00
Ariel Rin
256a21f058 Merge branch 'fix_table_styles_for_dark_mode' into 'master'
Fix table styles and nav tabs for dark mode

See merge request allianceauth/allianceauth!1310
2021-05-09 05:25:45 +00:00
Erik Kalkoken
1473259e41 Fix table styles and nav tabs for dark mode 2021-05-09 05:25:45 +00:00
Ariel Rin
e935b91e93 Merge branch 'improve_gitlab_error_handling' into 'master'
Improve handling of gitlab HTTP errors

See merge request allianceauth/allianceauth!1311
2021-05-09 05:17:56 +00:00
ErikKalkoken
07258a6914 Log HTTP errors from gitlab as warning instead of error 2021-05-08 13:20:57 +02:00
Ariel Rin
cb35808508 Merge branch 'improve_ci_script' into 'master'
Remove redis ping and consolidate before steps

See merge request allianceauth/allianceauth!1309
2021-05-05 09:04:43 +00:00
Erik Kalkoken
937d656227 Remove redis ping and consolidate before steps 2021-05-05 09:04:43 +00:00
Ariel Rin
75a3adb2c9 Merge branch 'cron-fix' into 'master'
[Docs] Fix Discord Task Schedule

See merge request allianceauth/allianceauth!1304
2021-04-27 13:53:37 +00:00
colcrunch
148e208476 Update cron schedule for discord.update_all_usernames task in docs. 2021-04-18 22:14:52 -04:00
Ariel Rin
0036e8b280 Version Bump v2.8.3 2021-04-07 15:18:08 +00:00
Ariel Rin
ea2b5bfecf Merge branch 'srp-killmail-link-fix-issue-1282' into 'master'
Proper error message on non kill mail zkb links

See merge request allianceauth/allianceauth!1297
2021-04-07 15:12:56 +00:00
Peter Pfeufer
aa7495fa60 Proper error message on non kill mail zkb links 2021-04-07 15:12:56 +00:00
Ariel Rin
162ec1bd86 Merge branch 'docs-permissioncleanup' into 'master'
Permissions Cleanup Docs

See merge request allianceauth/allianceauth!1293
2021-04-07 13:23:28 +00:00
Ariel Rin
2668884008 Merge branch 'srp-copypaste' into 'master'
SRP Character Name Copy-Paste

See merge request allianceauth/allianceauth!1295
2021-04-07 13:22:24 +00:00
Ariel Rin
abdc3f3485 SRP Character Name Copy-Paste 2021-04-07 13:22:23 +00:00
Ariel Rin
911f21ee7c Merge branch 'add-support-discord-link' into 'master'
support discord link added to admin notifications on dashboard

See merge request allianceauth/allianceauth!1298
2021-04-07 13:21:26 +00:00
Ariel Rin
2387c40254 Limit Django to 3.1.x 2021-04-07 13:12:15 +00:00
Peter Pfeufer
76e18a79b3 support discord link added to admin notifications on dashboard 2021-04-06 10:44:12 +02:00
Ariel Rin
9725c9c947 Update .gitlab-ci.yml file 2021-03-27 08:38:40 +00:00
Ariel Rin
247ed7cc64 Merge branch 'nootification_log_spam' into 'master'
Use default by default in Notification system

See merge request allianceauth/allianceauth!1294
2021-02-25 11:04:18 +00:00
Aaron Kable
2fd2af793e Use default by defaut in notification system 2021-02-21 18:14:42 +08:00
Ariel Rin
3fc36b9ce1 Permissions Cleanup Tool 2021-02-18 13:33:23 +10:00
Ariel Rin
c12fd2d7bc Merge branch 'improve_developer_docs' into 'master'
Improve developer docs

See merge request allianceauth/allianceauth!1292
2021-02-18 01:52:15 +00:00
ErikKalkoken
7fe1ba2fb2 Improve docs for WSL setup 2021-02-17 09:50:01 +01:00
ErikKalkoken
ab7eb3e5df Fix sphinx issues in docs 2021-02-17 09:35:18 +01:00
47 changed files with 1021 additions and 481 deletions

View File

@@ -1,43 +1,83 @@
stages: stages:
- gitlab
- test - test
- deploy - deploy
include:
- template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
before_script: before_script:
- apt-get update && apt-get install redis-server -y - apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes - redis-server --daemonize yes
- redis-cli ping - python -V
- python -V - pip install wheel tox
- pip install wheel tox
sast:
stage: gitlab
before_script: []
dependency_scanning:
stage: gitlab
before_script:
- apt-get update && apt-get install redis-server libmariadbclient-dev -y
- redis-server --daemonize yes
- python -V
- pip install wheel tox
test-3.6-core: test-3.6-core:
image: python:3.6-buster image: python:3.6-buster
script: script:
- tox -e py36-core - tox -e py36-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.7-core: test-3.7-core:
image: python:3.7-buster image: python:3.7-buster
script: script:
- tox -e py37-core - tox -e py37-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-core: test-3.8-core:
image: python:3.8-buster image: python:3.8-buster
script: script:
- tox -e py38-core - tox -e py38-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.6-all: test-3.6-all:
image: python:3.6-buster image: python:3.6-buster
script: script:
- tox -e py36-all - tox -e py36-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.7-all: test-3.7-all:
image: python:3.7-buster image: python:3.7-buster
script: script:
- tox -e py37-all - tox -e py37-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-all: test-3.8-all:
image: python:3.8-buster image: python:3.8-buster
script: script:
- tox -e py38-all - tox -e py38-all
artifacts:
when: always
reports:
cobertura: coverage.xml
deploy_production: deploy_production:
stage: deploy stage: deploy
@@ -51,4 +91,4 @@ deploy_production:
- twine upload dist/* - twine upload dist/*
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG

View File

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

View File

@@ -1,10 +1,8 @@
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User as BaseUser, \ from django.contrib.auth.models import User as BaseUser, \
Permission as BasePermission, Group Permission as BasePermission, Group
from django.db.models import Q, F from django.db.models import Count, Q
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, \ from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed post_delete, m2m_changed
@@ -24,11 +22,6 @@ from allianceauth.eveonline.tasks import update_character
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \ from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
else:
_has_auto_groups = False
def make_service_hooks_update_groups_action(service): def make_service_hooks_update_groups_action(service):
""" """
@@ -91,8 +84,7 @@ class UserProfileInline(admin.StackedInline):
if request.user.is_superuser: if request.user.is_superuser:
query |= Q(userprofile__isnull=True) query |= Q(userprofile__isnull=True)
else: else:
query |= Q(character_ownership__user=obj) query |= Q(character_ownership__user=obj)
qs = EveCharacter.objects.filter(query)
formset = super().get_formset(request, obj=obj, **kwargs) formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index): def get_kwargs(self, index):
@@ -121,6 +113,8 @@ def user_profile_pic(obj):
) )
else: else:
return None return None
user_profile_pic.short_description = '' user_profile_pic.short_description = ''
@@ -152,6 +146,7 @@ def user_username(obj):
user_obj.username, user_obj.username,
) )
user_username.short_description = 'user / main' user_username.short_description = 'user / main'
user_username.admin_order_field = 'username' user_username.admin_order_field = 'username'
@@ -168,7 +163,8 @@ def user_main_organization(obj):
else: else:
corporation = user_obj.profile.main_character.corporation_name corporation = user_obj.profile.main_character.corporation_name
if user_obj.profile.main_character.alliance_id: if user_obj.profile.main_character.alliance_id:
result = format_html('{}<br>{}', result = format_html(
'{}<br>{}',
corporation, corporation,
user_obj.profile.main_character.alliance_name user_obj.profile.main_character.alliance_name
) )
@@ -176,6 +172,7 @@ def user_main_organization(obj):
result = corporation result = corporation
return result return result
user_main_organization.short_description = 'Corporation / Alliance (Main)' user_main_organization.short_description = 'Corporation / Alliance (Main)'
user_main_organization.admin_order_field = \ user_main_organization.admin_order_field = \
'profile__main_character__corporation_name' 'profile__main_character__corporation_name'
@@ -205,13 +202,13 @@ class MainCorporationsFilter(admin.SimpleListFilter):
return qs.all() return qs.all()
else: else:
if qs.model == User: if qs.model == User:
return qs\ return qs.filter(
.filter(profile__main_character__corporation_id=\ profile__main_character__corporation_id=self.value()
self.value()) )
else: else:
return qs\ return qs.filter(
.filter(user__profile__main_character__corporation_id=\ user__profile__main_character__corporation_id=self.value()
self.value()) )
class MainAllianceFilter(admin.SimpleListFilter): class MainAllianceFilter(admin.SimpleListFilter):
@@ -239,12 +236,11 @@ class MainAllianceFilter(admin.SimpleListFilter):
return qs.all() return qs.all()
else: else:
if qs.model == User: if qs.model == User:
return qs\ return qs.filter(profile__main_character__alliance_id=self.value())
.filter(profile__main_character__alliance_id=self.value())
else: else:
return qs\ return qs.filter(
.filter(user__profile__main_character__alliance_id=\ user__profile__main_character__alliance_id=self.value()
self.value()) )
def update_main_character_model(modeladmin, request, queryset): def update_main_character_model(modeladmin, request, queryset):
@@ -259,6 +255,7 @@ def update_main_character_model(modeladmin, request, queryset):
'Update from ESI started for {} characters'.format(tasks_count) 'Update from ESI started for {} characters'.format(tasks_count)
) )
update_main_character_model.short_description = \ update_main_character_model.short_description = \
'Update main character model from ESI' 'Update main character model from ESI'
@@ -267,32 +264,16 @@ class UserAdmin(BaseUserAdmin):
"""Extending Django's UserAdmin model """Extending Django's UserAdmin model
Behavior of groups and characters columns can be configured via settings Behavior of groups and characters columns can be configured via settings
""" """
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("authentication/css/admin.css",)
} }
class RealGroupsFilter(admin.SimpleListFilter): def get_queryset(self, request):
"""Custom filter to get groups w/o Autogroups""" qs = super().get_queryset(request)
title = 'group' return qs.prefetch_related("character_ownerships__character", "groups")
parameter_name = 'group_id__exact'
def lookups(self, request, model_admin):
qs = Group.objects.all().order_by(Lower('name'))
if _has_auto_groups:
qs = qs\
.filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True)
return tuple([(x.pk, x.name) for x in qs])
def queryset(self, request, queryset):
if self.value() is None:
return queryset.all()
else:
return queryset.filter(groups__pk=self.value())
def get_actions(self, request): def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request) actions = super(BaseUserAdmin, self).get_actions(request)
@@ -341,11 +322,9 @@ class UserAdmin(BaseUserAdmin):
return result return result
inlines = BaseUserAdmin.inlines + [UserProfileInline] inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
ordering = ('username', ) list_select_related = ('profile__state', 'profile__main_character')
list_select_related = True show_full_result_count = True
show_full_result_count = True
list_display = ( list_display = (
user_profile_pic, user_profile_pic,
user_username, user_username,
@@ -358,10 +337,9 @@ class UserAdmin(BaseUserAdmin):
'_role' '_role'
) )
list_display_links = None list_display_links = None
list_filter = ( list_filter = (
'profile__state', 'profile__state',
RealGroupsFilter, 'groups',
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
'is_active', 'is_active',
@@ -375,41 +353,25 @@ class UserAdmin(BaseUserAdmin):
) )
def _characters(self, obj): def _characters(self, obj):
my_characters = [ character_ownerships = list(obj.character_ownerships.all())
x.character.character_name characters = [obj.character.character_name for obj in character_ownerships]
for x in CharacterOwnership.objects\
.filter(user=obj)\
.order_by('character__character_name')\
.select_related()
]
return self._list_2_html_w_tooltips( return self._list_2_html_w_tooltips(
my_characters, sorted(characters),
AUTHENTICATION_ADMIN_USERS_MAX_CHARS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
) )
_characters.short_description = 'characters' _characters.short_description = 'characters'
def _state(self, obj): def _state(self, obj):
return obj.profile.state.name return obj.profile.state.name
_state.short_description = 'state' _state.short_description = 'state'
_state.admin_order_field = 'profile__state' _state.admin_order_field = 'profile__state'
def _groups(self, obj): def _groups(self, obj):
if not _has_auto_groups: my_groups = sorted([group.name for group in list(obj.groups.all())])
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( return self._list_2_html_w_tooltips(
my_groups, my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
) )
_groups.short_description = 'groups' _groups.short_description = 'groups'
@@ -437,7 +399,7 @@ class UserAdmin(BaseUserAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form""" """overriding this formfield to have sorted lists in the form"""
if db_field.name == "groups": if db_field.name == "groups":
kwargs["queryset"] = Group.objects.all().order_by(Lower('name')) kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
@@ -446,9 +408,14 @@ class StateAdmin(admin.ModelAdmin):
list_select_related = True list_select_related = True
list_display = ('name', 'priority', '_user_count') list_display = ('name', 'priority', '_user_count')
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(user_count=Count("userprofile__id"))
def _user_count(self, obj): def _user_count(self, obj):
return obj.userprofile_set.all().count() return obj.user_count
_user_count.short_description = 'Users' _user_count.short_description = 'Users'
_user_count.admin_order_field = 'user_count'
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -471,7 +438,7 @@ class StateAdmin(admin.ModelAdmin):
] ]
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form""" """overriding this formfield to have sorted lists in the form"""
if db_field.name == "member_characters": if db_field.name == "member_characters":
kwargs["queryset"] = EveCharacter.objects.all()\ kwargs["queryset"] = EveCharacter.objects.all()\
.order_by(Lower('character_name')) .order_by(Lower('character_name'))
@@ -481,8 +448,10 @@ class StateAdmin(admin.ModelAdmin):
elif db_field.name == "member_alliances": elif db_field.name == "member_alliances":
kwargs["queryset"] = EveAllianceInfo.objects.all()\ kwargs["queryset"] = EveAllianceInfo.objects.all()\
.order_by(Lower('alliance_name')) .order_by(Lower('alliance_name'))
elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
if obj == get_guest_state(): if obj == get_guest_state():
return False return False
@@ -504,7 +473,8 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
"all": ("authentication/css/admin.css",) "all": ("authentication/css/admin.css",)
} }
list_select_related = True list_select_related = (
'user__profile__state', 'user__profile__main_character', 'character')
list_display = ( list_display = (
user_profile_pic, user_profile_pic,
user_username, user_username,
@@ -542,6 +512,7 @@ class CharacterOwnershipAdmin(BaseOwnershipAdmin):
class PermissionAdmin(admin.ModelAdmin): class PermissionAdmin(admin.ModelAdmin):
actions = None actions = None
readonly_fields = [field.name for field in BasePermission._meta.fields] readonly_fields = [field.name for field in BasePermission._meta.fields]
search_fields = ('codename', )
list_display = ('admin_name', 'name', 'codename', 'content_type') list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',) list_filter = ('content_type__app_label',)

View File

@@ -10,9 +10,10 @@ def get_admin_change_view_url(obj: object) -> str:
args=(obj.pk,) args=(obj.pk,)
) )
def get_admin_search_url(ModelClass: type) -> str: def get_admin_search_url(ModelClass: type) -> str:
"""returns URL to search URL for model of given object""" """returns URL to search URL for model of given object"""
return '{}{}/'.format( return '{}{}/'.format(
reverse('admin:app_list', args=(ModelClass._meta.app_label,)), reverse('admin:app_list', args=(ModelClass._meta.app_label,)),
ModelClass.__name__.lower() ModelClass.__name__.lower()
) )

View File

@@ -1,10 +1,8 @@
from urllib.parse import quote from urllib.parse import quote
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User as BaseUser, Group from django.contrib.auth.models import Group
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
from allianceauth.authentication.models import ( from allianceauth.authentication.models import (
@@ -18,8 +16,7 @@ from allianceauth.tests.auth_utils import AuthUtils
from ..admin import ( from ..admin import (
BaseUserAdmin, BaseUserAdmin,
CharacterOwnershipAdmin, CharacterOwnershipAdmin,
PermissionAdmin,
StateAdmin, StateAdmin,
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
@@ -35,11 +32,6 @@ from ..admin import (
) )
from . import get_admin_change_view_url, get_admin_search_url from . import get_admin_change_view_url, get_admin_search_url
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
else:
_has_auto_groups = False
MODULE_PATH = 'allianceauth.authentication.admin' MODULE_PATH = 'allianceauth.authentication.admin'
@@ -48,6 +40,7 @@ class MockRequest(object):
def __init__(self, user=None): def __init__(self, user=None):
self.user = user self.user = user
class TestCaseWithTestData(TestCase): class TestCaseWithTestData(TestCase):
@classmethod @classmethod
@@ -279,6 +272,7 @@ class TestStateAdmin(TestCaseWithTestData):
expected = 200 expected = 200
self.assertEqual(response.status_code, expected) self.assertEqual(response.status_code, expected)
class TestUserAdmin(TestCaseWithTestData): class TestUserAdmin(TestCaseWithTestData):
def setUp(self): def setUp(self):
@@ -287,24 +281,12 @@ class TestUserAdmin(TestCaseWithTestData):
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
self.character_1 = self.user_1.character_ownerships.first().character self.character_1 = self.user_1.character_ownerships.first().character
def _create_autogroups(self):
"""create autogroups for corps and alliances"""
if _has_auto_groups:
autogroups_config = AutogroupsConfig(
corp_groups = True,
alliance_groups = True
)
autogroups_config.save()
for state in State.objects.all():
autogroups_config.states.add(state)
autogroups_config.update_corp_group_membership(self.user_1)
# column rendering
def test_user_profile_pic_u1(self): def test_user_profile_pic_u1(self):
expected = ('<img src="https://images.evetech.net/characters/1001/' expected = (
'portrait?size=32" class="img-circle">') '<img src="https://images.evetech.net/characters/1001/'
'portrait?size=32" class="img-circle">'
)
self.assertEqual(user_profile_pic(self.user_1), expected) self.assertEqual(user_profile_pic(self.user_1), expected)
def test_user_profile_pic_u3(self): def test_user_profile_pic_u3(self):
@@ -351,37 +333,17 @@ class TestUserAdmin(TestCaseWithTestData):
result = self.modeladmin._characters(self.user_3) result = self.modeladmin._characters(self.user_3)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u1(self): def test_groups_u1(self):
self._create_autogroups()
expected = 'Group 1' expected = 'Group 1'
result = self.modeladmin._groups(self.user_1) result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u2(self): def test_groups_u2(self):
self._create_autogroups()
expected = 'Group 2' expected = 'Group 2'
result = self.modeladmin._groups(self.user_2) result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u3(self): 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) result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result) self.assertIsNone(result)
@@ -413,8 +375,10 @@ class TestUserAdmin(TestCaseWithTestData):
def test_list_2_html_w_tooltips_w_cutoff(self): def test_list_2_html_w_tooltips_w_cutoff(self):
items = ['one', 'two', 'three'] items = ['one', 'two', 'three']
expected = ('<span data-tooltip="one, two, three" ' expected = (
'class="tooltip">one, two, (...)</span>') '<span data-tooltip="one, two, three" '
'class="tooltip">one, two, (...)</span>'
)
result = self.modeladmin._list_2_html_w_tooltips(items, 2) result = self.modeladmin._list_2_html_w_tooltips(items, 2)
self.assertEqual(expected, result) self.assertEqual(expected, result)
@@ -439,63 +403,7 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertTrue(mock_message_user.called) self.assertTrue(mock_message_user.called)
# filters # 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): def test_filter_main_corporations(self):
class UserAdminTest(BaseUserAdmin): class UserAdminTest(BaseUserAdmin):
@@ -603,7 +511,6 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def sync_nicknames_bulk(self, user): def sync_nicknames_bulk(self, user):
pass pass
def test_service_has_update_groups_only(self): def test_service_has_update_groups_only(self):
service = self.MyServicesHookTypeA() service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)

View File

@@ -1,10 +1,10 @@
from math import ceil from math import ceil
from unittest.mock import patch from unittest.mock import patch
from requests import RequestException
import requests_mock import requests_mock
from packaging.version import Version as Pep440Version from packaging.version import Version as Pep440Version
from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from allianceauth.templatetags.admin_status import ( from allianceauth.templatetags.admin_status import (
@@ -12,8 +12,7 @@ from allianceauth.templatetags.admin_status import (
_fetch_list_from_gitlab, _fetch_list_from_gitlab,
_current_notifications, _current_notifications,
_current_version_summary, _current_version_summary,
_fetch_notification_issues_from_gitlab, _fetch_notification_issues_from_gitlab,
_fetch_tags_from_gitlab,
_latests_versions _latests_versions
) )
@@ -103,35 +102,51 @@ class TestStatusOverviewTag(TestCase):
class TestNotifications(TestCase): class TestNotifications(TestCase):
def setUp(self) -> None:
cache.clear()
@requests_mock.mock() @requests_mock.mock()
def test_fetch_notification_issues_from_gitlab(self, requests_mocker): def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
# given
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement' '?labels=announcement'
) )
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES) requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
# when
result = _fetch_notification_issues_from_gitlab() result = _fetch_notification_issues_from_gitlab()
# then
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES) self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_normal(self, mock_cache): def test_current_notifications_normal(self, mock_cache):
# given
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5]) self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
@patch(MODULE_PATH + '.admin_status.cache') @requests_mock.mock()
def test_current_notifications_failed(self, mock_cache): def test_current_notifications_failed(self, requests_mocker):
mock_cache.get_or_set.side_effect = RequestException # given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement'
)
requests_mocker.get(url, status_code=404)
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], list()) self.assertEqual(result['notifications'], list())
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_is_none(self, mock_cache): def test_current_notifications_is_none(self, mock_cache):
# given
mock_cache.get_or_set.return_value = None mock_cache.get_or_set.return_value = None
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], list()) self.assertEqual(result['notifications'], list())
@@ -143,12 +158,17 @@ class TestCeleryQueueLength(TestCase):
class TestVersionTags(TestCase): class TestVersionTags(TestCase):
def setUp(self) -> None:
cache.clear()
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_normal(self, mock_cache): def test_current_version_info_normal(self, mock_cache):
# given
mock_cache.get_or_set.return_value = GITHUB_TAGS mock_cache.get_or_set.return_value = GITHUB_TAGS
# when
result = _current_version_summary() result = _current_version_summary()
# then
self.assertTrue(result['latest_major']) self.assertTrue(result['latest_major'])
self.assertTrue(result['latest_minor']) self.assertTrue(result['latest_minor'])
self.assertTrue(result['latest_patch']) self.assertTrue(result['latest_patch'])
@@ -158,32 +178,41 @@ class TestVersionTags(TestCase):
self.assertEqual(result['latest_beta_version'], '2.4.6a1') self.assertEqual(result['latest_beta_version'], '2.4.6a1')
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @requests_mock.mock()
def test_current_version_info_failed(self, mock_cache): def test_current_version_info_failed(self, requests_mocker):
mock_cache.get_or_set.side_effect = RequestException # given
url = (
expected = {} 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags'
)
requests_mocker.get(url, status_code=500)
# when
result = _current_version_summary() result = _current_version_summary()
self.assertEqual(result, expected) # then
self.assertEqual(result, {})
@requests_mock.mock() @requests_mock.mock()
def test_fetch_tags_from_gitlab(self, requests_mocker): def test_fetch_tags_from_gitlab(self, requests_mocker):
# given
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags' '/repository/tags'
) )
requests_mocker.get(url, json=GITHUB_TAGS) requests_mocker.get(url, json=GITHUB_TAGS)
result = _fetch_tags_from_gitlab() # when
self.assertEqual(result, GITHUB_TAGS) result = _current_version_summary()
# then
self.assertTrue(result)
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_return_no_data(self, mock_cache): def test_current_version_info_return_no_data(self, mock_cache):
mock_cache.get_or_set.return_value = None # given
mock_cache.get_or_set.return_value = None
expected = {} # when
result = _current_version_summary() result = _current_version_summary()
self.assertEqual(result, expected) # then
self.assertEqual(result, {})
class TestLatestsVersion(TestCase): class TestLatestsVersion(TestCase):

View File

@@ -96,24 +96,62 @@ class EveAllianceForm(EveEntityForm):
@admin.register(EveCorporationInfo) @admin.register(EveCorporationInfo)
class EveCorporationInfoAdmin(admin.ModelAdmin): class EveCorporationInfoAdmin(admin.ModelAdmin):
search_fields = ['corporation_name']
list_display = ('corporation_name', 'alliance')
list_select_related = ('alliance',)
list_filter = (('alliance', admin.RelatedOnlyFieldListFilter),)
ordering = ('corporation_name',)
def has_change_permission(self, request, obj=None):
return False
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk: if not obj or not obj.pk:
return EveCorporationForm return EveCorporationForm
return super(EveCorporationInfoAdmin, self).get_form(request, obj=obj, **kwargs) return super().get_form(request, obj=obj, **kwargs)
@admin.register(EveAllianceInfo) @admin.register(EveAllianceInfo)
class EveAllianceInfoAdmin(admin.ModelAdmin): class EveAllianceInfoAdmin(admin.ModelAdmin):
search_fields = ['alliance_name']
list_display = ('alliance_name',)
ordering = ('alliance_name',)
def has_change_permission(self, request, obj=None):
return False
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk: if not obj or not obj.pk:
return EveAllianceForm return EveAllianceForm
return super(EveAllianceInfoAdmin, self).get_form(request, obj=obj, **kwargs) return super().get_form(request, obj=obj, **kwargs)
@admin.register(EveCharacter) @admin.register(EveCharacter)
class EveCharacterAdmin(admin.ModelAdmin): class EveCharacterAdmin(admin.ModelAdmin):
search_fields = ['character_name', 'corporation_name', 'alliance_name', 'character_ownership__user__username'] search_fields = [
list_display = ('character_name', 'corporation_name', 'alliance_name', 'user', 'main_character') 'character_name',
'corporation_name',
'alliance_name',
'character_ownership__user__username'
]
list_display = (
'character_name', 'corporation_name', 'alliance_name', 'user', 'main_character'
)
list_select_related = (
'character_ownership', 'character_ownership__user__profile__main_character'
)
list_filter = (
'corporation_name',
'alliance_name',
(
'character_ownership__user__profile__main_character',
admin.RelatedOnlyFieldListFilter
),
)
ordering = ('character_name', )
def has_change_permission(self, request, obj=None):
return False
@staticmethod @staticmethod
def user(obj): def user(obj):

View File

@@ -32,10 +32,12 @@ class EveCharacterManager(models.Manager):
def update_character(self, character_id): def update_character(self, character_id):
return self.get(character_id=character_id).update_character() return self.get(character_id=character_id).update_character()
def get_character_by_id(self, char_id): def get_character_by_id(self, character_id: int):
if self.filter(character_id=char_id).exists(): """Return character by character ID or None if not found."""
return self.get(character_id=char_id) try:
return None return self.get(character_id=character_id)
except self.model.DoesNotExist:
return None
class EveAllianceProviderManager: class EveAllianceProviderManager:

View File

@@ -1,5 +1,5 @@
from django.conf import settings from django.apps import apps
from django.contrib.auth.models import Permission
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User from django.contrib.auth.models import Group as BaseGroup, User
from django.db.models import Count from django.db.models import Count
@@ -10,9 +10,8 @@ from django.dispatch import receiver
from .models import AuthGroup from .models import AuthGroup
from .models import GroupRequest from .models import GroupRequest
from . import signals
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS: if 'eve_autogroups' in apps.app_configs:
_has_auto_groups = True _has_auto_groups = True
else: else:
_has_auto_groups = False _has_auto_groups = False
@@ -41,9 +40,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
kwargs["queryset"] = Group.objects.order_by(Lower('name')) kwargs["queryset"] = Group.objects.order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return False return False
@@ -97,9 +93,9 @@ class HasLeaderFilter(admin.SimpleListFilter):
else: else:
return queryset return queryset
class GroupAdmin(admin.ModelAdmin):
list_select_related = True class GroupAdmin(admin.ModelAdmin):
ordering = ('name', ) ordering = ('name',)
list_display = ( list_display = (
'name', 'name',
'_description', '_description',
@@ -118,9 +114,12 @@ class GroupAdmin(admin.ModelAdmin):
list_filter.append(HasLeaderFilter) list_filter.append(HasLeaderFilter)
search_fields = ('name', 'authgroup__description') search_fields = ('name', 'authgroup__description')
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
if _has_auto_groups:
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
qs = qs.annotate( qs = qs.annotate(
member_count=Count('user', distinct=True), member_count=Count('user', distinct=True),
) )
@@ -136,7 +135,7 @@ class GroupAdmin(admin.ModelAdmin):
_member_count.admin_order_field = 'member_count' _member_count.admin_order_field = 'member_count'
def has_leader(self, obj): def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
has_leader.boolean = True has_leader.boolean = True
@@ -166,6 +165,18 @@ class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',) filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,) inlines = (AuthGroupInlineAdmin,)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs)
def save_formset(self, request, form, formset, change):
for inline_form in formset:
ag_instance = inline_form.save(commit=False)
ag_instance.group = form.instance
ag_instance.save()
formset.save()
class Group(BaseGroup): class Group(BaseGroup):
class Meta: class Meta:
@@ -173,13 +184,29 @@ class Group(BaseGroup):
verbose_name = BaseGroup._meta.verbose_name verbose_name = BaseGroup._meta.verbose_name
verbose_name_plural = BaseGroup._meta.verbose_name_plural verbose_name_plural = BaseGroup._meta.verbose_name_plural
try: try:
admin.site.unregister(BaseGroup) admin.site.unregister(BaseGroup)
finally: finally:
admin.site.register(Group, GroupAdmin) admin.site.register(Group, GroupAdmin)
admin.site.register(GroupRequest) @admin.register(GroupRequest)
class GroupRequestAdmin(admin.ModelAdmin):
search_fields = ('user__username', )
list_display = ('id', 'group', 'user', '_leave_request', 'status')
list_filter = (
('group', admin.RelatedOnlyFieldListFilter),
('user', admin.RelatedOnlyFieldListFilter),
'leave_request',
'status'
)
def _leave_request(self, obj) -> True:
return obj.leave_request
_leave_request.short_description = 'is leave request'
_leave_request.boolean = True
@receiver(pre_save, sender=Group) @receiver(pre_save, sender=Group)

View File

@@ -5,3 +5,6 @@ class GroupManagementConfig(AppConfig):
name = 'allianceauth.groupmanagement' name = 'allianceauth.groupmanagement'
label = 'groupmanagement' label = 'groupmanagement'
verbose_name = 'Group Management' verbose_name = 'Group Management'
def ready(self):
from . import signals # noqa: F401

View File

@@ -55,7 +55,6 @@ class RequestLog(models.Model):
return user.profile.main_character return user.profile.main_character
class AuthGroup(models.Model): class AuthGroup(models.Model):
""" """
Extends Django Group model with a one-to-one field Extends Django Group model with a one-to-one field
@@ -107,7 +106,8 @@ class AuthGroup(models.Model):
help_text="States listed here will have the ability to join this group provided " help_text="States listed here will have the ability to join this group provided "
"they have the proper permissions.") "they have the proper permissions.")
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)</i> of the group shown to users.") description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
"</i> of the group shown to users.")
def __str__(self): def __str__(self):
return self.group.name return self.group.name

View File

@@ -40,120 +40,121 @@
</li> </li>
</ul> </ul>
<div class="tab-content"> <div class="panel panel-default panel-tabs-aa">
<div id="add" class="tab-pane fade in active panel panel-default"> <div class="panel-body">
<div class="panel-body"> <div class="tab-content">
{% if acceptrequests %}
<div class="table-responsive"> <div id="add" class="tab-pane active">
<table class="table table-aa"> {% if acceptrequests %}
<thead> <div class="table-responsive">
<tr> <table class="table table-aa">
<th>{% trans "Character" %}</th> <thead>
<th>{% trans "Organization" %}</th>
<th>{% trans "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for acceptrequest in acceptrequests %}
<tr> <tr>
<td> <th>{% trans "Character" %}</th>
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;"> <th>{% trans "Organization" %}</th>
{% if acceptrequest.main_char %} <th>{% trans "Group" %}</th>
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank"> <th></th>
{{ acceptrequest.main_char.character_name }}
</a>
{% else %}
{{ acceptrequest.user.username }}
{% endif %}
</td>
<td>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
{% endif %}
</div>
</div>
<div id="leave" class="tab-pane fade panel panel-default"> <tbody>
<div class="panel-body"> {% for acceptrequest in acceptrequests %}
{% if leaverequests %} <tr>
<div class="table-responsive"> <td>
<table class="table table-aa"> <img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
<thead> {% if acceptrequest.main_char %}
<tr> <a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
<th>{% trans "Character" %}</th> {{ acceptrequest.main_char.character_name }}
<th>{% trans "Organization" %}</th> </a>
<th>{% trans "Group" %}</th> {% else %}
<th></th> {{ acceptrequest.user.username }}
</tr> {% endif %}
</thead> </td>
<td>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<tbody> <a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
{% for leaverequest in leaverequests %} {% trans "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group add requests." %}</div>
{% endif %}
</div>
<div id="leave" class="tab-pane">
{% if leaverequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr> <tr>
<td> <th>{% trans "Character" %}</th>
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;"> <th>{% trans "Organization" %}</th>
{% if leaverequest.main_char %} <th>{% trans "Group" %}</th>
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank"> <th></th>
{{ leaverequest.main_char.character_name }}
</a>
{% else %}
{{ leaverequest.user.username }}
{% endif %}
</td>
<td>
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ leaverequest.main_char.corporation_name }}
</a><br>
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ leaverequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody>
</table> <tbody>
</div> {% for leaverequest in leaverequests %}
{% else %} <tr>
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div> <td>
{% endif %} <img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;">
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
{{ leaverequest.main_char.character_name }}
</a>
{% else %}
{{ leaverequest.user.username }}
{% endif %}
</td>
<td>
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ leaverequest.main_char.corporation_name }}
</a><br>
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% trans "(unknown)" %}
{% endif %}
</td>
<td>{{ leaverequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
{% trans "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% trans "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No group leave requests." %}</div>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -47,7 +47,8 @@ class TestGroupAdmin(TestCase):
# group 2 - no leader # group 2 - no leader
cls.group_2 = Group.objects.create(name='Group 2') cls.group_2 = Group.objects.create(name='Group 2')
cls.group_2.authgroup.description = 'Internal Group' cls.group_2.authgroup.description = 'Internal Group'
cls.group_2.authgroup.internal = True cls.group_2.authgroup.internal = True
cls.group_2.authgroup.group_leader_groups.add(cls.group_1)
cls.group_2.authgroup.save() cls.group_2.authgroup.save()
# group 3 - has leader # group 3 - has leader
@@ -237,10 +238,14 @@ class TestGroupAdmin(TestCase):
result = self.modeladmin._member_count(obj) result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_has_leader(self): def test_has_leader_user(self):
result = self.modeladmin.has_leader(self.group_1) result = self.modeladmin.has_leader(self.group_1)
self.assertTrue(result) self.assertTrue(result)
def test_has_leader_group(self):
result = self.modeladmin.has_leader(self.group_2)
self.assertTrue(result)
def test_properties_1(self): def test_properties_1(self):
expected = ['Default'] expected = ['Default']
result = self.modeladmin._properties(self.group_1) result = self.modeladmin._properties(self.group_1)

View File

@@ -1,4 +1,35 @@
from django.contrib import admin from django.contrib import admin
from .models import Notification from .models import Notification
admin.site.register(Notification)
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ("timestamp", "_main", "_state", "title", "level", "viewed")
list_select_related = ("user", "user__profile__main_character", "user__profile__state")
list_filter = (
"level",
"timestamp",
"user__profile__state",
('user__profile__main_character', admin.RelatedOnlyFieldListFilter),
)
ordering = ("-timestamp", )
search_fields = ["user__username", "user__profile__main_character__character_name"]
def _main(self, obj):
try:
return obj.user.profile.main_character
except AttributeError:
return obj.user
_main.admin_order_field = "user__profile__main_character__character_name"
def _state(self, obj):
return obj.user.profile.state
_state.admin_order_field = "user__profile__state__name"
def has_change_permission(self, request, obj=None):
return False
def has_add_permission(self, request) -> bool:
return False

View File

@@ -12,21 +12,20 @@ class NotificationHandler(logging.Handler):
try: try:
perm = Permission.objects.get(codename="logging_notifications") perm = Permission.objects.get(codename="logging_notifications")
message = record.getMessage()
if record.exc_text:
message += "\n\n"
message = message + record.exc_text
users = User.objects.filter(
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
for user in users:
notify(
user,
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
level=str([item[0] for item in Notification.LEVEL_CHOICES if item[1] == record.levelname][0]),
message=message
)
except Permission.DoesNotExist: except Permission.DoesNotExist:
pass return
message = record.getMessage()
if record.exc_text:
message += "\n\n"
message = message + record.exc_text
users = User.objects.filter(
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
for user in users:
notify(
user,
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
level=Notification.Level.from_old_name(record.levelname),
message=message
)

View File

@@ -12,7 +12,7 @@ class NotificationQuerySet(models.QuerySet):
"""Custom QuerySet for Notification model""" """Custom QuerySet for Notification model"""
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
# overriden update to ensure cache is invaidated on very call """Override update to ensure cache is invalidated on very call."""
super().update(*args, **kwargs) super().update(*args, **kwargs)
user_pks = set(self.select_related("user").values_list('user__pk', flat=True)) user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
for user_pk in user_pks: for user_pk in user_pks:
@@ -43,6 +43,8 @@ class NotificationManager(models.Manager):
if not message: if not message:
message = title message = title
if level not in self.model.Level:
level = self.model.Level.INFO
obj = self.create(user=user, title=title, message=message, level=level) obj = self.create(user=user, title=title, message=message, level=level)
logger.info("Created notification %s", obj) logger.info("Created notification %s", obj)
return obj return obj

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.12 on 2021-07-01 21:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('notifications', '0004_performance_tuning'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='level',
field=models.CharField(choices=[('danger', 'danger'), ('warning', 'warning'), ('info', 'info'), ('success', 'success')], default='info', max_length=10),
),
]

View File

@@ -2,6 +2,7 @@ import logging
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from .managers import NotificationManager from .managers import NotificationManager
@@ -13,17 +14,43 @@ class Notification(models.Model):
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50 NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30 NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
LEVEL_CHOICES = ( class Level(models.TextChoices):
('danger', 'CRITICAL'), """A notification level."""
('danger', 'ERROR'),
('warning', 'WARN'), DANGER = 'danger', _('danger') #:
('info', 'INFO'), WARNING = 'warning', _('warning') #:
('success', 'DEBUG'), INFO = 'info', _('info') #:
) SUCCESS = 'success', _('success') #:
@classmethod
def from_old_name(cls, name: str) -> object:
"""Map old name to enum.
Raises ValueError for invalid names.
"""
name_map = {
"CRITICAL": cls.DANGER,
"ERROR": cls.DANGER,
"WARN": cls.WARNING,
"INFO": cls.INFO,
"DEBUG": cls.SUCCESS,
}
try:
return name_map[name]
except KeyError:
raise ValueError(f"Unknown name: {name}") from None
# LEVEL_CHOICES = (
# ('danger', 'CRITICAL'),
# ('danger', 'ERROR'),
# ('warning', 'WARN'),
# ('info', 'INFO'),
# ('success', 'DEBUG'),
# )
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
level = models.CharField(choices=LEVEL_CHOICES, max_length=10) level = models.CharField(choices=Level.choices, max_length=10, default=Level.INFO)
title = models.CharField(max_length=254) title = models.CharField(max_length=254)
message = models.TextField() message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True) timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
@@ -45,22 +72,15 @@ class Notification(models.Model):
Notification.objects.invalidate_user_notification_cache(self.user.pk) Notification.objects.invalidate_user_notification_cache(self.user.pk)
def mark_viewed(self) -> None: def mark_viewed(self) -> None:
"""mark notification as viewed""" """Mark notification as viewed."""
logger.info("Marking notification as viewed: %s" % self) logger.info("Marking notification as viewed: %s" % self)
self.viewed = True self.viewed = True
self.save() self.save()
def set_level(self, level_name: str) -> None: def set_level(self, level_name: str) -> None:
"""set notification level according to level name, e.g. 'CRITICAL' """Set notification level according to old level name, e.g. 'CRITICAL'.
raised exception on invalid level names Raises ValueError on invalid level names.
""" """
try: self.level = self.Level.from_old_name(level_name)
new_level = [
item[0] for item in self.LEVEL_CHOICES if item[1] == level_name
][0]
except IndexError:
raise ValueError('Invalid level name: %s' % level_name)
self.level = new_level
self.save() self.save()

View File

@@ -33,10 +33,8 @@ def user_unread_notification_count(user: object) -> int:
@register.simple_tag @register.simple_tag
def notifications_refresh_time() -> int: def notifications_refresh_time() -> int:
refresh_time = getattr(settings, 'NOTIFICATIONS_REFRESH_TIME', None) refresh_time = getattr(settings, 'NOTIFICATIONS_REFRESH_TIME', Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT)
if ( if (not isinstance(refresh_time, int) or refresh_time < 0):
refresh_time is None or not isinstance(refresh_time, int) or refresh_time < 0
):
logger.warning('NOTIFICATIONS_REFRESH_TIME setting is invalid. Using default.') logger.warning('NOTIFICATIONS_REFRESH_TIME setting is invalid. Using default.')
refresh_time = Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT refresh_time = Notification.NOTIFICATIONS_REFRESH_TIME_DEFAULT

View File

@@ -0,0 +1,69 @@
from logging import LogRecord, DEBUG
from django.contrib.auth.models import Permission, Group, User
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..handlers import NotificationHandler
from ..models import Notification
MODULE_PATH = 'allianceauth.notifications.handlers'
class TestHandler(TestCase):
def test_do_nothing_if_permission_does_not_exist(self):
# given
Permission.objects.get(codename="logging_notifications").delete()
handler = NotificationHandler()
record = LogRecord(
name="name",
level=DEBUG,
pathname="pathname",
lineno=42,
msg="msg",
args=[],
exc_info=None,
func="func"
)
# when
handler.emit(record)
# then
self.assertEqual(Notification.objects.count(), 0)
def test_should_emit_message_to_users_with_permission_only(self):
# given
AuthUtils.create_user('Lex Luthor')
user_permission = AuthUtils.create_user('Bruce Wayne')
user_permission = AuthUtils.add_permission_to_user_by_name(
"auth.logging_notifications", user_permission
)
group = Group.objects.create(name="Dummy Group")
perm = Permission.objects.get(codename="logging_notifications")
group.permissions.add(perm)
user_group = AuthUtils.create_user('Peter Parker')
user_group.groups.add(group)
user_superuser = User.objects.create_superuser("Clark Kent")
handler = NotificationHandler()
record = LogRecord(
name="name",
level=DEBUG,
pathname="pathname",
lineno=42,
msg="msg",
args=[],
exc_info=None,
func="func"
)
# when
handler.emit(record)
# then
self.assertEqual(Notification.objects.count(), 3)
users = set(Notification.objects.values_list("user__pk", flat=True))
self.assertSetEqual(
users, {user_permission.pk, user_group.pk, user_superuser.pk}
)
notif = Notification.objects.first()
self.assertEqual(notif.user, user_permission)
self.assertEqual(notif.title, "DEBUG [func:42]")
self.assertEqual(notif.level, "success")
self.assertEqual(notif.message, "msg")

View File

@@ -64,6 +64,35 @@ class TestUserNotify(TestCase):
self.assertEqual(obj.user, self.user) self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title) self.assertEqual(obj.title, title)
self.assertEqual(obj.message, title) self.assertEqual(obj.message, title)
def test_should_use_default_level_when_not_specified(self):
# given
title = 'dummy_title'
message = 'dummy message'
# when
Notification.objects.notify_user(self.user, title, message)
# then
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title)
self.assertEqual(obj.message, message)
self.assertEqual(obj.level, Notification.Level.INFO)
def test_should_use_default_level_when_invalid_level_given(self):
# given
title = 'dummy_title'
message = 'dummy message'
level = "invalid"
# when
Notification.objects.notify_user(self.user, title, message, level)
# then
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, title)
self.assertEqual(obj.message, message)
self.assertEqual(obj.level, Notification.Level.INFO)
@override_settings(NOTIFICATIONS_MAX_PER_USER=3) @override_settings(NOTIFICATIONS_MAX_PER_USER=3)
def test_remove_when_too_many_notifications(self): def test_remove_when_too_many_notifications(self):

View File

@@ -83,10 +83,31 @@ class DiscordUserManager(models.Manager):
if created is not False: if created is not False:
if created is None: if created is None:
logger.debug( logger.debug(
"User %s with Discord ID %s is already a member.", "User %s with Discord ID %s is already a member. Forcing a Refresh",
user, user,
user_id, user_id,
) )
# Force an update cause the discord API won't do it for us.
if role_ids:
role_ids = list(role_ids)
updated = bot_client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=user_id,
role_ids=role_ids,
nick=nickname
)
if not updated:
# Could not update the new user so fail.
logger.warning(
"Failed to add user %s with Discord ID %s to Discord server",
user,
user_id,
)
return False
self.update_or_create( self.update_or_create(
user=user, user=user,
defaults={ defaults={

View File

@@ -111,6 +111,40 @@ class TestAddUser(TestCase):
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3}) self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick']) self.assertIsNone(kwargs['nick'])
def test_can_activate_existing_user_with_roles_no_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
roles = [
create_matched_role(ROLE_ALPHA),
create_matched_role(ROLE_BRAVO),
create_matched_role(ROLE_CHARLIE)
]
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = ['a', 'b', 'c']
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = roles
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True) @patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_create_user_no_roles_with_nick( def test_can_create_user_no_roles_with_nick(
self, self,
@@ -140,6 +174,36 @@ class TestAddUser(TestCase):
self.assertIsNone(kwargs['role_ids']) self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME) self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_activate_existing_user_no_roles_with_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False) @patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False)
def test_can_create_user_no_roles_and_without_nick_if_turned_off( def test_can_create_user_no_roles_and_without_nick_if_turned_off(
self, self,
@@ -183,6 +247,7 @@ class TestAddUser(TestCase):
mock_DiscordClient.return_value.match_or_create_roles_from_names\ mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = [] .return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef') result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result) self.assertTrue(result)
@@ -190,6 +255,31 @@ class TestAddUser(TestCase):
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists() DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
) )
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called) self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_can_activate_existing_guild_member_failure(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = False
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertFalse(result)
self.assertFalse(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_return_false_when_user_creation_fails( def test_return_false_when_user_creation_fails(
self, self,

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.1.6 on 2021-03-23 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mumble', '0001_squashed_0011_auto_20201011_1009'),
]
operations = [
migrations.AddField(
model_name='mumbleuser',
name='last_connect',
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', max_length=254, null=True, verbose_name='Last Connection'),
),
migrations.AddField(
model_name='mumbleuser',
name='last_disconnect',
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection to Mumble', max_length=254, null=True, verbose_name='Last Disconnection'),
),
migrations.AddField(
model_name='mumbleuser',
name='release',
field=models.TextField(blank=True, editable=False, help_text='The Mumble Release the user last authenticated with', max_length=254, null=True, verbose_name='Mumble Release'),
),
migrations.AddField(
model_name='mumbleuser',
name='version',
field=models.IntegerField(blank=True, editable=False, help_text='Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.', null=True, verbose_name='Mumble Version'),
),
]

View File

@@ -74,7 +74,41 @@ class MumbleUser(AbstractServiceModel):
editable=False, editable=False,
help_text="Hash of Mumble client certificate as presented when user authenticates" help_text="Hash of Mumble client certificate as presented when user authenticates"
) )
display_name = models.CharField(max_length=254, unique=True) display_name = models.CharField(
max_length=254,
unique=True
)
release = models.TextField(
verbose_name="Mumble Release",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="The Mumble Release the user last authenticated with"
)
version = models.IntegerField(
verbose_name="Mumble Version",
blank=True,
null=True,
editable=False,
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203."
)
last_connect = models.DateTimeField(
verbose_name="Last Connection",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="Timestamp of the users Last Connection to Mumble"
)
last_disconnect = models.DateTimeField(
verbose_name="Last Disconnection",
max_length=254,
blank=True,
null=True,
editable=False,
help_text="Timestamp of the users Last Disconnection to Mumble"
)
objects = MumbleManager() objects = MumbleManager()

View File

@@ -15,6 +15,7 @@ class Teamspeak3UserAdmin(ServicesUserAdmin):
@admin.register(AuthTS) @admin.register(AuthTS)
class AuthTSgroupAdmin(admin.ModelAdmin): class AuthTSgroupAdmin(admin.ModelAdmin):
change_list_template = 'admin/teamspeak3/authts/change_list.html'
ordering = ('auth_group__name', ) ordering = ('auth_group__name', )
list_select_related = True list_select_related = True
@@ -28,7 +29,7 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
return [x for x in obj.ts_group.all().order_by('ts_group_id')] return [x for x in obj.ts_group.all().order_by('ts_group_id')]
_ts_group.short_description = 'ts groups' _ts_group.short_description = 'ts groups'
#_ts_group.admin_order_field = 'profile__state' # _ts_group.admin_order_field = 'profile__state'
@admin.register(StateGroup) @admin.register(StateGroup)

View File

@@ -0,0 +1,11 @@
{% extends "admin/change_list.html" %}
{% load i18n static %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a href="{% url 'teamspeak3:admin_update_ts3_groups' %}" class="btn btn-high">
Update TS3 groups
</a>
</li>
{% endblock %}

View File

@@ -216,6 +216,24 @@ class Teamspeak3ViewsTestCase(TestCase):
self.assertEqual(ts3_user.perm_key, '123abc') self.assertEqual(ts3_user.perm_key, '123abc')
self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called) self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
@mock.patch(MODULE_PATH + '.views.Teamspeak3Tasks')
@mock.patch(MODULE_PATH + '.views.messages')
def test_should_update_ts_groups(self, messages, Teamspeak3Tasks):
# given
self.member.is_superuser = True
self.member.is_staff = True
self.member.save()
self.login()
# when
response = self.client.get(urls.reverse('teamspeak3:admin_update_ts3_groups'))
# then
self.assertRedirects(
response, urls.reverse('admin:teamspeak3_authts_changelist'),
target_status_code=200
)
self.assertTrue(messages.info.called)
self.assertTrue(Teamspeak3Tasks.run_ts3_group_update.delay.called)
class Teamspeak3SignalsTestCase(TestCase): class Teamspeak3SignalsTestCase(TestCase):
def setUp(self): def setUp(self):

View File

@@ -6,12 +6,14 @@ app_name = 'teamspeak3'
module_urls = [ module_urls = [
# Teamspeak3 service control # Teamspeak3 service control
url(r'^activate/$', views.activate_teamspeak3, url(r'^activate/$', views.activate_teamspeak3, name='activate'),
name='activate'), url(r'^deactivate/$', views.deactivate_teamspeak3, name='deactivate'),
url(r'^deactivate/$', views.deactivate_teamspeak3, url(r'^reset_perm/$', views.reset_teamspeak3_perm, name='reset_perm'),
name='deactivate'), url(
url(r'^reset_perm/$', views.reset_teamspeak3_perm, r'^admin_update_ts3_groups/$',
name='reset_perm'), views.admin_update_ts3_groups,
name='admin_update_ts3_groups'
),
# Teamspeak Urls # Teamspeak Urls
url(r'^verify/$', views.verify_teamspeak3, name='verify'), url(r'^verify/$', views.verify_teamspeak3, name='verify'),

View File

@@ -1,6 +1,7 @@
import logging import logging
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.conf import settings from django.conf import settings
@@ -99,3 +100,11 @@ def reset_teamspeak3_perm(request):
logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user) logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user)
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.')) messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services") return redirect("services:services")
@login_required
@staff_member_required
def admin_update_ts3_groups(request):
Teamspeak3Tasks.run_ts3_group_update.delay()
messages.info(request, "Started updating TS3 server groups...")
return redirect("admin:teamspeak3_authts_changelist")

View File

@@ -1,3 +1,5 @@
import re
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -21,6 +23,11 @@ class SrpFleetUserRequestForm(forms.Form):
data = self.cleaned_data['killboard_link'] data = self.cleaned_data['killboard_link']
if "zkillboard.com" not in data: if "zkillboard.com" not in data:
raise forms.ValidationError(_("Invalid Link. Please use zKillboard.com")) raise forms.ValidationError(_("Invalid Link. Please use zKillboard.com"))
if not re.match(r"http[s]?://zkillboard\.com/kill/\d+\/", data):
raise forms.ValidationError(
_("Invalid Link. Please post a direct link to a killmail.")
)
return data return data

View File

@@ -10,6 +10,9 @@
{% include 'bundles/x-editable.css.html' %} {% include 'bundles/x-editable.css.html' %}
<link href="{% static 'css/checkbox.css' %}" rel="stylesheet" type="text/css"> <link href="{% static 'css/checkbox.css' %}" rel="stylesheet" type="text/css">
<style> <style>
.copy-text-fa-icon:hover {
cursor: pointer;
}
.radio label, .checkbox label { .radio label, .checkbox label {
padding-left: 10px; padding-left: 10px;
} }
@@ -109,7 +112,8 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{{ srpfleetrequest.character.alliance.alliance_ticker }} {{ srpfleetrequest.character.alliance.alliance_ticker }}
{% endif %} {% endif %}
[{{ srpfleetrequest.character.corporation.corporation_ticker }}] [{{ srpfleetrequest.character.corporation.corporation_ticker }}]
{{ srpfleetrequest.character.character_name }} {{ srpfleetrequest.character.character_name }}&nbsp;<i class="copy-text-fa-icon far fa-copy" data-clipboard-text="{{ srpfleetrequest.character.character_name }}"></i>
</span>
</td> </td>
<td class="text-center"> <td class="text-center">
<a href="{{ srpfleetrequest.killboard_link }}" <a href="{{ srpfleetrequest.killboard_link }}"
@@ -182,7 +186,24 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js.html' %}
{% include 'bundles/x-editable-js.html' %} {% include 'bundles/x-editable-js.html' %}
{% include 'bundles/moment-js.html' %} {% include 'bundles/moment-js.html' %}
{% endblock %} {% include 'bundles/clipboard-js.html' %}
<script>
var clipboard = new ClipboardJS('.copy-text-fa-icon');
clipboard.on('success', function (e) {
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
e.clearSelection();
});
clipboard.on('error', function (e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});
</script>
{% endblock extra_javascript %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function() { $(document).ready(function() {

View File

@@ -43,27 +43,51 @@ ul.list-group.list-group-horizontal > li.list-group-item {
justify-content: center; justify-content: center;
} }
/* style group headers within a table */ @media all {
.tr-group { /* style nav tabs in dark mode*/
font-weight: bold; .template-dark-mode .nav-tabs > li.active > a {
background-color: #e6e6e6 !important; background-color: rgb(70, 69, 69)!important;
} color: rgb(255, 255, 255) !important;
}
/* default style for tables */ .panel-tabs-aa {
.table-aa > thead > tr > th{ border-top: none;
border-bottom: 1px solid #f2f2f2; border-top-left-radius: 0%;
} border-top-right-radius: 0%;
.table-aa > thead > tr > th{ }
vertical-align: middle;
} /* style group headers within a table */
.table-aa > tbody > tr > td{ .template-light-mode .tr-group {
border-bottom: 1px solid #f2f2f2; font-weight: bold;
} background-color: #e6e6e6 !important;
.table-aa > tbody > tr > td { }
vertical-align: middle; .template-dark-mode .tr-group {
} font-weight: bold;
.table-aa > tbody > tr:last-child { background-color: rgb(105, 105, 105) !important;
border-bottom: none; }
/* default style for tables */
.template-light-mode .table-aa > thead > tr > th{
border-bottom: 1px solid #f2f2f2;
}
.template-dark-mode .table-aa > thead > tr > th{
border-bottom: 1px solid rgb(70, 69, 69);
}
.table-aa > thead > tr > th{
vertical-align: middle;
}
.template-light-mode .table-aa > tbody > tr > td{
border-bottom: 1px solid #f2f2f2;
}
.template-dark-mode .table-aa > tbody > tr > td{
border-bottom: 1px solid rgb(70, 69, 69);
}
.table-aa > tbody > tr > td {
vertical-align: middle;
}
.table-aa > tbody > tr:last-child {
border-bottom: none;
}
} }
/* highlight active menu items /* highlight active menu items

View File

@@ -18,9 +18,20 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="text-right" style="position:absolute;bottom:5px;right:5px;">
<a href="https://gitlab.com/allianceauth/allianceauth/issues"><span class="label" style="background-color:#e65328;"> <div class="text-right" style="position: absolute; bottom: 5px; right: 5px;">
<i class="fab fa-gitlab" aria-hidden="true"></i> Powered by GitLab</span> <a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" style="margin-right: 0.5rem;">
<span class="label" style="background-color: #e65328;">
<i class="fab fa-gitlab" aria-hidden="true"></i>
{% translate 'Powered by GitLab' %}
</span>
</a>
<a href="https://discord.com/invite/fjnHAmk" target="_blank">
<span class="label" style="background-color: rgb(110,133,211);">
<i class="fab fa-discord" aria-hidden="true"></i>
{% translate 'Support Discord' %}
</span>
</a> </a>
</div> </div>
</div> </div>

View File

@@ -30,15 +30,7 @@
<div class="row" id="site-body-wrapper"> <div class="row" id="site-body-wrapper">
{% include 'allianceauth/side-menu.html' %} {% include 'allianceauth/side-menu.html' %}
<div class="col-sm-10"> <div class="col-sm-10">
{% if messages %} {% include 'allianceauth/messages.html' %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }}">{{ message }}</div>
{% if not forloop.last %}
<br>
{% endif %}
{% endfor %}
{% endif %}
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
</div> </div>

View File

@@ -0,0 +1,26 @@
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<div class="message-icon pull-left" style="margin-right: 0.5rem;">
{% if message.level_tag == "info" %}
<i class="fas fa-info-circle"></i>
{% elif message.level_tag == "success" %}
<i class="fas fa-check-circle"></i>
{% elif message.level_tag == "warning" %}
<i class="fas fa-exclamation-circle"></i>
{% elif message.level_tag == "danger" %}
<i class="fas fa-exclamation-triangle"></i>
{% endif %}
</div>
<div class="message-text" style="margin-left: 2.5rem;">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}

View File

@@ -1,3 +1,3 @@
<!-- Start Clipboard.js js from cdnjs --> <!-- Start Clipboard.js js from cdnjs -->
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.6/clipboard.min.js"></script> <script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js" integrity="sha512-sIqUEnRn31BgngPmHt2JenzleDDsXwYO+iyvQ46Mw6RL+udAUZj2n/u/PGY80NxRxynO7R9xIGx5LEzw4INWJQ==" crossorigin="anonymous"></script>
<!-- End Clipboard.js js from cdnjs --> <!-- End Clipboard.js js from cdnjs -->

View File

@@ -70,8 +70,8 @@ def _current_notifications() -> dict:
_fetch_notification_issues_from_gitlab, _fetch_notification_issues_from_gitlab,
NOTIFICATION_CACHE_TIME NOTIFICATION_CACHE_TIME
) )
except requests.RequestException: except requests.HTTPError:
logger.exception('Error while getting gitlab notifications') logger.warning('Error while getting gitlab notifications', exc_info=True)
top_notifications = [] top_notifications = []
else: else:
if notifications: if notifications:
@@ -95,8 +95,8 @@ def _current_version_summary() -> dict:
tags = cache.get_or_set( tags = cache.get_or_set(
'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME 'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME
) )
except requests.RequestException: except requests.HTTPError:
logger.exception('Error while getting gitlab release tags') logger.warning('Error while getting gitlab release tags', exc_info=True)
return {} return {}
if not tags: if not tags:

View File

@@ -23,7 +23,7 @@ Within your auth project exists two folders named `static` and `templates`. Thes
You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory. You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory.
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allinceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included. It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allianceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
## Custom URLs and Views ## Custom URLs and Views

View File

@@ -25,6 +25,11 @@ The development environment consists of the following components:
We will use the build-in Django development webserver, so we don't need to setup a WSGI server or a web server. We will use the build-in Django development webserver, so we don't need to setup a WSGI server or a web server.
```eval_rst
.. note::
This setup works with both WSL 1 and WSL 2. However, due to the significantly better performance we recommend WSL 2.
```
## Requirement ## Requirement
The only requirement is a PC with Windows 10 and Internet connection in order to download the additional software components. The only requirement is a PC with Windows 10 and Internet connection in order to download the additional software components.
@@ -361,8 +366,7 @@ Here is an example debug config for Celery:
"module": "celery", "module": "celery",
"cwd": "${workspaceFolder}/myauth", "cwd": "${workspaceFolder}/myauth",
"console": "integratedTerminal", "console": "integratedTerminal",
"args": [ "args": [
"-E",
"-A", "-A",
"myauth", "myauth",
"worker", "worker",
@@ -371,7 +375,8 @@ Here is an example debug config for Celery:
"-P", "-P",
"solo", "solo",
], ],
"django": true "django": true,
"justMyCode": true,
}, },
``` ```
@@ -386,15 +391,14 @@ Finally it makes sense to have a dedicated debug config for running unit tests.
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/myauth/manage.py", "program": "${workspaceFolder}/myauth/manage.py",
"args": [ "args": [
"test", "test",
"-v 2",
"--keepdb", "--keepdb",
"--debug-mode", "--debug-mode",
"--failfast", "--failfast",
"example", "example",
], ],
"django": true,
"django": true "justMyCode": true
}, },
``` ```
@@ -416,13 +420,31 @@ Finally you may also want to have a debug config to debug a non-Django Python sc
## Additional tools ## Additional tools
The following additional tools are very helpful when developing for AA. The following additional tools are very helpful when developing for AA with VS Code:
### Pylance
Pylance is an extension that works alongside Python in Visual Studio Code to provide performant language support: [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
### Code Spell Checker ### Code Spell Checker
Typos in your user facing comments can be quite embarrassing. This spell checker helps you avoid them. Typos in your user facing comments can be quite embarrassing. This spell checker helps you avoid them: [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)
Install from here: [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) ### markdownlint
Extension for Visual Studio Code - Markdown linting and style checking for Visual Studio Code: [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
### GitLens
Extension for Visual Studio Code - Supercharge the Git capabilities built into Visual Studio Code: [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
### RST preview
A VS Code extension to preview restructured text and provide syntax highlighting: [RST Preview](https://marketplace.visualstudio.com/items?itemName=tht13.rst-vscode)
### Django Template
This extension adds language colorization support and user snippets for the Django template language to VS Code: [Django Template](https://marketplace.visualstudio.com/items?itemName=bibhasdn.django-html)
### DBeaver ### DBeaver

View File

@@ -7,19 +7,16 @@ The notifications package has an API for sending notifications.
Location: ``allianceauth.notifications`` Location: ``allianceauth.notifications``
.. automodule:: allianceauth.notifications.__init__ .. automodule:: allianceauth.notifications.__init__
:members: notify :members: notify
:undoc-members:
models models
=========== ===========
.. autoclass:: allianceauth.notifications.models.Notification .. autoclass:: allianceauth.notifications.models.Notification
:members: view, set_level, LEVEL_CHOICES :members: Level, mark_viewed, set_level
:undoc-members:
managers managers
=========== ===========
.. autoclass:: allianceauth.notifications.managers.NotificationManager .. autoclass:: allianceauth.notifications.managers.NotificationManager
:members: notify_user, user_unread_count :members: notify_user, user_unread_count
:undoc-members:

View File

@@ -42,11 +42,12 @@ Auto Groups are configured via models in the Admin Interface, a user will requir
+-------------------------------------------+------------------+----------------+ +-------------------------------------------+------------------+----------------+
| Permission | Admin Site | Auth Site | | Permission | Admin Site | Auth Site |
+===========================================+==================+================+ +===========================================+==================+================+
| eve_autogroups.add_autogroupsconfig | Can create model | None. | | eve_autogroups.add_autogroupsconfig | Can create model | None. |
+-------------------------------------------+------------------+----------------+ +-------------------------------------------+------------------+----------------+
| eve_autogroups.change_autogroupsconfig | Can edit model | None. | | eve_autogroups.change_autogroupsconfig | Can edit model | None. |
+-------------------------------------------+------------------+----------------+ +-------------------------------------------+------------------+----------------+
| eve_autogroups.delete_autogroupsconfig | Can delete model | None. | | eve_autogroups.delete_autogroupsconfig | Can delete model | None. |
+-------------------------------------------+------------------+----------------+ +-------------------------------------------+------------------+----------------+
``` ```
There exists more models that will be automatically created and maintained by this module, they do not require end-user/admin interaction. `managedalliancegroup` `managedcorpgroups` There exists more models that will be automatically created and maintained by this module, they do not require end-user/admin interaction. `managedalliancegroup` `managedcorpgroups`

View File

@@ -26,7 +26,7 @@ DISCORD_SYNC_NAMES = False
CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = { CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
'task': 'discord.update_all_usernames', 'task': 'discord.update_all_usernames',
'schedule': crontab(hour='*/12'), 'schedule': crontab(minute=0, hour='*/12'),
} }
``` ```

View File

@@ -38,14 +38,14 @@ To install we need a copy of the server. You can find the latest version from [t
Download the server, replacing the link with the link you got earlier. Download the server, replacing the link with the link you got earlier.
```url ```text
http://dl.4players.de/ts/releases/3.13.2/teamspeak3-server_linux_amd64-3.13.2.tar.bz2 http://dl.4players.de/ts/releases/3.13.2/teamspeak3-server_linux_amd64-3.13.2.tar.bz2
``` ```
Now we need to extract the file. Now we need to extract the file.
```bash ```bash
tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2 tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2
``` ```
### Create User ### Create User
@@ -105,7 +105,9 @@ Click the URL provided to automatically connect to our server. It will prompt yo
Now we need to make groups. AllianceAuth handles groups in teamspeak differently: instead of creating groups it creates an association between groups in TeamSpeak and groups in AllianceAuth. Go ahead and make the groups you want to associate with auth groups, keeping in mind multiple TeamSpeak groups can be associated with a single auth group. Now we need to make groups. AllianceAuth handles groups in teamspeak differently: instead of creating groups it creates an association between groups in TeamSpeak and groups in AllianceAuth. Go ahead and make the groups you want to associate with auth groups, keeping in mind multiple TeamSpeak groups can be associated with a single auth group.
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Services`, select `Auth / TS Groups`. In the top-right corner click `Add`. Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Teamspeak3`, select `Auth / TS Groups`.
In the top-right corner click, first click on `Update TS3 Groups` to fetch the newly created server groups from TS3 (this may take a minute to complete). Then click on `Add Auth / TS Group` to link Auth groups with TS3 server groups.
The dropdown box provides all auth groups. Select one and assign TeamSpeak groups from the panels below. If these panels are empty, wait a minute for the database update to run, or see the [troubleshooting section](#ts-group-models-not-populating-on-admin-site) below. The dropdown box provides all auth groups. Select one and assign TeamSpeak groups from the panels below. If these panels are empty, wait a minute for the database update to run, or see the [troubleshooting section](#ts-group-models-not-populating-on-admin-site) below.
@@ -119,13 +121,23 @@ To enable advanced permissions, on your client go to the `Tools` menu, `Applicat
### TS group models not populating on admin site ### TS group models not populating on admin site
The method which populates these runs every 30 minutes. To populate manually, start a django shell: The method which populates these runs every 30 minutes. To populate manually you start the process from the admin site or from the Django shell.
#### Admin Site
Navigate to the AllianceAuth admin interface and under `Teamspeak3`, select `Auth / TS Groups`.
Then, in the top-right corner click, click on `Update TS3 Groups` to start the process of fetching the server groups from TS3 (this may take a minute to complete).
#### Django Shell
Start a django shell with:
```bash ```bash
python manage.py shell python manage.py shell
``` ```
And execute the update: And execute the update as follows:
```python ```python
from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks

View File

@@ -104,6 +104,8 @@ CREATE DATABASE alliance_auth CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost'; GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost';
``` ```
Once your database is set up, you can leave the SQL shell with `exit`.
Add timezone tables to your mysql installation: Add timezone tables to your mysql installation:
```bash ```bash
@@ -177,7 +179,7 @@ source /home/allianceserver/venv/auth/bin/activate
You need to have a dedicated Eve SSO app for Alliance auth. Please go to [EVE Developer](https://developers.eveonline.com/applications) to create one. You need to have a dedicated Eve SSO app for Alliance auth. Please go to [EVE Developer](https://developers.eveonline.com/applications) to create one.
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used. For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience, we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
As **callback URL** you want to define the URL of your Alliance Auth site plus the route: `/sso/callback`. Example for a valid callback URL: `https://auth.example.com/sso/callback` As **callback URL** you want to define the URL of your Alliance Auth site plus the route: `/sso/callback`. Example for a valid callback URL: `https://auth.example.com/sso/callback`
@@ -236,7 +238,7 @@ Check to ensure your settings are valid.
python /home/allianceserver/myauth/manage.py check python /home/allianceserver/myauth/manage.py check
``` ```
And finally ensure the allianceserver user has read/write permissions to this directory before proceeding. Finally, ensure the allianceserver user has read/write permissions to this directory before proceeding.
```bash ```bash
chown -R allianceserver:allianceserver /home/allianceserver/myauth chown -R allianceserver:allianceserver /home/allianceserver/myauth
@@ -244,7 +246,7 @@ chown -R allianceserver:allianceserver /home/allianceserver/myauth
## Services ## Services
Alliance Auth needs some additional services to run, which we will setup and configure next. Alliance Auth needs some additional services to run, which we will set up and configure next.
### Gunicorn ### Gunicorn
@@ -275,7 +277,7 @@ systemctl enable supervisord.service
systemctl start supervisord.service systemctl start supervisord.service
``` ```
Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running. Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
Ubuntu: Ubuntu:
@@ -289,7 +291,7 @@ CentOS:
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
``` ```
And activate it with `supervisorctl reload`. Activate it with `supervisorctl reload`.
You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process. You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process.
@@ -304,11 +306,11 @@ You can check the status of the processes with `supervisorctl status`. Logs from
Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide. Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide.
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use and some functions (e.g. Email confirmation links) will not work properly. Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use, and some functions (e.g. Email confirmation links) will not work properly.
## Superuser ## Superuser
Before using your auth site it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account. Before using your auth site, it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
```bash ```bash
python /home/allianceserver/myauth/manage.py createsuperuser python /home/allianceserver/myauth/manage.py createsuperuser
@@ -316,7 +318,7 @@ python /home/allianceserver/myauth/manage.py createsuperuser
The superuser account is accessed by logging in via the admin site at `https://example.com/admin`. The superuser account is accessed by logging in via the admin site at `https://example.com/admin`.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added it is possible to use SSO to login to this account. If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added, it is possible to use SSO to login to this account.
## Updating ## Updating
@@ -340,7 +342,7 @@ Some releases come with new or changed models. Update your database to reflect t
python /home/allianceserver/myauth/manage.py migrate python /home/allianceserver/myauth/manage.py migrate
``` ```
Finally some releases come with new or changed static files. Run the following command to update your static files folder: Finally, some releases come with new or changed static files. Run the following command to update your static files folder:
```bash ```bash
python /home/allianceserver/myauth/manage.py collectstatic python /home/allianceserver/myauth/manage.py collectstatic

View File

@@ -1,4 +1,6 @@
# Adding and Removing Apps # App Maintenance
## Adding and Removing Apps
Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but here is the general procedure: Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but here is the general procedure:
@@ -8,3 +10,17 @@ Your auth project is just a regular Django project - you can add in [other Djang
4. restart AA with `supervisorctl restart myauth:` 4. restart AA with `supervisorctl restart myauth:`
If you ever want to remove an app, you should first clear it from the database to avoid dangling foreign keys: `python manage.py migrate appname zero`. Then you can remove it from your auth project's `INSTALLED_APPS` list. If you ever want to remove an app, you should first clear it from the database to avoid dangling foreign keys: `python manage.py migrate appname zero`. Then you can remove it from your auth project's `INSTALLED_APPS` list.
## Permission Cleanup
Mature Alliance Auth installations, or those with actively developed extensions may find themselves with stale or duplicated Permission models.
This can make it confusing for admins to apply the right permissions, contribute to larger queries in backend management or simply look unsightly.
```shell
python manage.py remove_stale_contenttypes --include-stale-apps
```
This inbuilt Django command will step through each contenttype and offer to delete it, displaying what exactly this will cascade to delete. Pay attention and ensure you understand exactly what is being removed before answering `yes`.
This should only cleanup uninstalled apps, deprecated permissions within apps should be cleaned up using Data Migrations by each responsible application.

View File

@@ -22,12 +22,12 @@ install_requires = [
'celery>=4.3.0,<5.0.0,!=4.4.4', # 4.4.4 is missing a dependency 'celery>=4.3.0,<5.0.0,!=4.4.4', # 4.4.4 is missing a dependency
'celery_once>=2.0.1', 'celery_once>=2.0.1',
'django>=3.1.1,<4.0.0', 'django>=3.1.1,<3.2.0',
'django-bootstrap-form', 'django-bootstrap-form',
'django-registration>=3.1', 'django-registration>=3.1',
'django-sortedm2m', 'django-sortedm2m',
'django-redis-cache>=3.0.0', 'django-redis-cache>=3.0.0',
'django-celery-beat>=2.0.0', 'django-celery-beat>=2.0.0,<2.2.1',
'openfire-restapi', 'openfire-restapi',
'sleekxmpp', 'sleekxmpp',
@@ -71,7 +71,7 @@ setup(
classifiers=[ classifiers=[
'Environment :: Web Environment', 'Environment :: Web Environment',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 2.2', 'Framework :: Django :: 3.1',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',