mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 12:30:15 +02:00
Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x
This commit is contained in:
commit
ff0fa0329d
@ -164,6 +164,12 @@ build-test:
|
|||||||
- dist/*
|
- dist/*
|
||||||
expire_in: 1 year
|
expire_in: 1 year
|
||||||
|
|
||||||
|
test-docs:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.10-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e docs
|
||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.10-bullseye
|
image: python:3.10-bullseye
|
||||||
|
@ -1,27 +1,62 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from typing import Optional, List
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from redis import Redis
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
|
from redis import Redis, RedisError
|
||||||
|
|
||||||
from django_redis import get_redis_connection
|
from django_redis import get_redis_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _RedisStub:
|
||||||
|
"""Stub of a Redis client.
|
||||||
|
|
||||||
|
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||||
|
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def incr(self, *args, **kwargs):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def zadd(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def zcount(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def zrangebyscore(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EventSeries:
|
class EventSeries:
|
||||||
"""API for recording and analysing a series of events."""
|
"""API for recording and analyzing a series of events."""
|
||||||
|
|
||||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||||
|
|
||||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
||||||
self._redis = get_redis_connection("default") if not redis else redis
|
self._redis = get_redis_connection("default") if not redis else redis
|
||||||
if not isinstance(self._redis, Redis):
|
try:
|
||||||
raise TypeError(
|
if not self._redis.ping():
|
||||||
"This class requires a Redis client, but none was provided "
|
raise RuntimeError()
|
||||||
"and the default Django cache backend is not Redis either."
|
except (AttributeError, RedisError, RuntimeError):
|
||||||
|
logger.exception(
|
||||||
|
"Failed to establish a connection with Redis. "
|
||||||
|
"This EventSeries object is disabled.",
|
||||||
)
|
)
|
||||||
|
self._redis = _RedisStub()
|
||||||
self._key_id = str(key_id)
|
self._key_id = str(key_id)
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disabled(self):
|
||||||
|
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
||||||
|
return isinstance(self._redis, _RedisStub)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _key_counter(self):
|
def _key_counter(self):
|
||||||
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
||||||
|
@ -1,13 +1,48 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
|
from redis import RedisError
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from allianceauth.authentication.task_statistics.event_series import EventSeries
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
|
EventSeries,
|
||||||
|
_RedisStub,
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||||
|
|
||||||
|
|
||||||
class TestEventSeries(TestCase):
|
class TestEventSeries(TestCase):
|
||||||
|
def test_should_abort_without_redis_client(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock:
|
||||||
|
mock.return_value = None
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertTrue(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
|
def test_should_disable_itself_if_redis_not_available_1(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
|
||||||
|
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertIsInstance(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
|
def test_should_disable_itself_if_redis_not_available_2(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
|
||||||
|
mock_get_master_client.return_value.ping.return_value = False
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertIsInstance(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
def test_should_add_event(self):
|
def test_should_add_event(self):
|
||||||
# given
|
# given
|
||||||
events = EventSeries("dummy")
|
events = EventSeries("dummy")
|
||||||
|
@ -212,7 +212,14 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
|||||||
start_of_previous_month = first_day_of_previous_month(year, month)
|
start_of_previous_month = first_day_of_previous_month(year, month)
|
||||||
|
|
||||||
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
|
||||||
user = EveCharacter.objects.get(character_id=char_id).user
|
try:
|
||||||
|
user = EveCharacter.objects.get(character_id=char_id).character_ownership.user
|
||||||
|
except EveCharacter.DoesNotExist:
|
||||||
|
messages.error(request, _('Character does not exist'))
|
||||||
|
return redirect('fatlink:view')
|
||||||
|
except AttributeError:
|
||||||
|
messages.error(request, _('User does not exist'))
|
||||||
|
return redirect('fatlink:view')
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import Group as BaseGroup
|
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth.models import Group as BaseGroup, Permission, User
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Exists, OuterRef
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models.signals import (
|
from django.db.models.signals import (
|
||||||
m2m_changed,
|
m2m_changed,
|
||||||
@ -15,6 +15,7 @@ from django.dispatch import receiver
|
|||||||
|
|
||||||
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
|
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
|
||||||
from .models import AuthGroup, GroupRequest, ReservedGroupName
|
from .models import AuthGroup, GroupRequest, ReservedGroupName
|
||||||
|
from .tasks import remove_users_not_matching_states_from_group
|
||||||
|
|
||||||
if 'eve_autogroups' in apps.app_configs:
|
if 'eve_autogroups' in apps.app_configs:
|
||||||
_has_auto_groups = True
|
_has_auto_groups = True
|
||||||
@ -106,14 +107,13 @@ class HasLeaderFilter(admin.SimpleListFilter):
|
|||||||
|
|
||||||
class GroupAdmin(admin.ModelAdmin):
|
class GroupAdmin(admin.ModelAdmin):
|
||||||
form = GroupAdminForm
|
form = GroupAdminForm
|
||||||
list_select_related = ('authgroup',)
|
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
list_display = (
|
list_display = (
|
||||||
'name',
|
'name',
|
||||||
'_description',
|
'_description',
|
||||||
'_properties',
|
'_properties',
|
||||||
'_member_count',
|
'_member_count',
|
||||||
'has_leader'
|
'has_leader',
|
||||||
)
|
)
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'authgroup__internal',
|
'authgroup__internal',
|
||||||
@ -129,31 +129,51 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
if _has_auto_groups:
|
has_leader_qs = (
|
||||||
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
|
AuthGroup.objects.filter(group=OuterRef('pk'), group_leaders__isnull=False)
|
||||||
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
|
|
||||||
qs = qs.annotate(
|
|
||||||
member_count=Count('user', distinct=True),
|
|
||||||
)
|
)
|
||||||
|
has_leader_groups_qs = (
|
||||||
|
AuthGroup.objects.filter(
|
||||||
|
group=OuterRef('pk'), group_leader_groups__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qs = (
|
||||||
|
qs.select_related('authgroup')
|
||||||
|
.annotate(member_count=Count('user', distinct=True))
|
||||||
|
.annotate(has_leader=Exists(has_leader_qs))
|
||||||
|
.annotate(has_leader_groups=Exists(has_leader_groups_qs))
|
||||||
|
)
|
||||||
|
if _has_auto_groups:
|
||||||
|
is_autogroup_corp = (
|
||||||
|
Group.objects.filter(
|
||||||
|
pk=OuterRef('pk'), managedcorpgroup__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is_autogroup_alliance = (
|
||||||
|
Group.objects.filter(
|
||||||
|
pk=OuterRef('pk'), managedalliancegroup__isnull=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qs = (
|
||||||
|
qs.annotate(is_autogroup_corp=Exists(is_autogroup_corp))
|
||||||
|
.annotate(is_autogroup_alliance=Exists(is_autogroup_alliance))
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _description(self, obj):
|
def _description(self, obj):
|
||||||
return obj.authgroup.description
|
return obj.authgroup.description
|
||||||
|
|
||||||
@admin.display(description="Members", ordering="member_count")
|
@admin.display(description='Members', ordering='member_count')
|
||||||
def _member_count(self, obj):
|
def _member_count(self, obj):
|
||||||
return obj.member_count
|
return obj.member_count
|
||||||
|
|
||||||
@admin.display(boolean=True)
|
@admin.display(boolean=True)
|
||||||
def has_leader(self, obj):
|
def has_leader(self, obj):
|
||||||
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
|
return obj.has_leader or obj.has_leader_groups
|
||||||
|
|
||||||
def _properties(self, obj):
|
def _properties(self, obj):
|
||||||
properties = list()
|
properties = list()
|
||||||
if _has_auto_groups and (
|
if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance):
|
||||||
obj.managedalliancegroup_set.exists()
|
|
||||||
or obj.managedcorpgroup_set.exists()
|
|
||||||
):
|
|
||||||
properties.append('Auto Group')
|
properties.append('Auto Group')
|
||||||
elif obj.authgroup.internal:
|
elif obj.authgroup.internal:
|
||||||
properties.append('Internal')
|
properties.append('Internal')
|
||||||
@ -183,6 +203,8 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
ag_instance = inline_form.save(commit=False)
|
ag_instance = inline_form.save(commit=False)
|
||||||
ag_instance.group = form.instance
|
ag_instance.group = form.instance
|
||||||
ag_instance.save()
|
ag_instance.save()
|
||||||
|
if ag_instance.states.exists():
|
||||||
|
remove_users_not_matching_states_from_group.delay(ag_instance.group.pk)
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
@ -189,6 +189,15 @@ class AuthGroup(models.Model):
|
|||||||
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
| User.objects.filter(groups__in=list(self.group_leader_groups.all()))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def remove_users_not_matching_states(self):
|
||||||
|
"""Remove users not matching defined states from related group."""
|
||||||
|
states_qs = self.states.all()
|
||||||
|
if states_qs.exists():
|
||||||
|
states = list(states_qs)
|
||||||
|
non_compliant_users = self.group.user_set.exclude(profile__state__in=states)
|
||||||
|
for user in non_compliant_users:
|
||||||
|
self.group.user_set.remove(user)
|
||||||
|
|
||||||
|
|
||||||
class ReservedGroupName(models.Model):
|
class ReservedGroupName(models.Model):
|
||||||
"""Name that can not be used for groups.
|
"""Name that can not be used for groups.
|
||||||
|
10
allianceauth/groupmanagement/tasks.py
Normal file
10
allianceauth/groupmanagement/tasks.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def remove_users_not_matching_states_from_group(group_pk: int) -> None:
|
||||||
|
"""Remove users not matching defined states from related group."""
|
||||||
|
group = Group.objects.get(pk=group_pk)
|
||||||
|
group.authgroup.remove_users_not_matching_states()
|
@ -6,7 +6,7 @@ from django.conf import settings
|
|||||||
from django.contrib import admin
|
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
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||||
|
|
||||||
from allianceauth.authentication.models import CharacterOwnership, State
|
from allianceauth.authentication.models import CharacterOwnership, State
|
||||||
from allianceauth.eveonline.models import (
|
from allianceauth.eveonline.models import (
|
||||||
@ -236,60 +236,104 @@ class TestGroupAdmin(TestCase):
|
|||||||
self.assertEqual(result, expected)
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
def test_member_count(self):
|
def test_member_count(self):
|
||||||
expected = 1
|
# given
|
||||||
obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\
|
request = MockRequest(user=self.user_1)
|
||||||
.get(pk=self.group_1.pk)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
result = self.modeladmin._member_count(obj)
|
result = self.modeladmin._member_count(obj)
|
||||||
self.assertEqual(result, expected)
|
# then
|
||||||
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
def test_has_leader_user(self):
|
def test_has_leader_user(self):
|
||||||
result = self.modeladmin.has_leader(self.group_1)
|
# given
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin.has_leader(obj)
|
||||||
|
# then
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_has_leader_group(self):
|
def test_has_leader_group(self):
|
||||||
result = self.modeladmin.has_leader(self.group_2)
|
# given
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin.has_leader(obj)
|
||||||
|
# then
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_properties_1(self):
|
def test_properties_1(self):
|
||||||
expected = ['Default']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_1)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Default'])
|
||||||
|
|
||||||
def test_properties_2(self):
|
def test_properties_2(self):
|
||||||
expected = ['Internal']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_2)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Internal'])
|
||||||
|
|
||||||
def test_properties_3(self):
|
def test_properties_3(self):
|
||||||
expected = ['Hidden']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_3)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_3.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Hidden'])
|
||||||
|
|
||||||
def test_properties_4(self):
|
def test_properties_4(self):
|
||||||
expected = ['Open']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_4)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_4.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Open'])
|
||||||
|
|
||||||
def test_properties_5(self):
|
def test_properties_5(self):
|
||||||
expected = ['Public']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_5)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_5.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Public'])
|
||||||
|
|
||||||
def test_properties_6(self):
|
def test_properties_6(self):
|
||||||
expected = ['Hidden', 'Open', 'Public']
|
# given
|
||||||
result = self.modeladmin._properties(self.group_6)
|
request = MockRequest(user=self.user_1)
|
||||||
self.assertListEqual(result, expected)
|
obj = self.modeladmin.get_queryset(request).get(pk=self.group_6.pk)
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
self.assertListEqual(result, ['Hidden', 'Open', 'Public'])
|
||||||
|
|
||||||
if _has_auto_groups:
|
if _has_auto_groups:
|
||||||
@patch(MODULE_PATH + '._has_auto_groups', True)
|
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||||
def test_properties_7(self):
|
def test_should_show_autogroup_for_corporation(self):
|
||||||
|
# given
|
||||||
self._create_autogroups()
|
self._create_autogroups()
|
||||||
expected = ['Auto Group']
|
request = MockRequest(user=self.user_1)
|
||||||
my_group = Group.objects\
|
queryset = self.modeladmin.get_queryset(request)
|
||||||
.filter(managedcorpgroup__isnull=False)\
|
obj = queryset.filter(managedcorpgroup__isnull=False).first()
|
||||||
.first()
|
# when
|
||||||
result = self.modeladmin._properties(my_group)
|
result = self.modeladmin._properties(obj)
|
||||||
self.assertListEqual(result, expected)
|
# then
|
||||||
|
self.assertListEqual(result, ['Auto Group'])
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '._has_auto_groups', True)
|
||||||
|
def test_should_show_autogroup_for_alliance(self):
|
||||||
|
# given
|
||||||
|
self._create_autogroups()
|
||||||
|
request = MockRequest(user=self.user_1)
|
||||||
|
queryset = self.modeladmin.get_queryset(request)
|
||||||
|
obj = queryset.filter(managedalliancegroup__isnull=False).first()
|
||||||
|
# when
|
||||||
|
result = self.modeladmin._properties(obj)
|
||||||
|
# then
|
||||||
|
self.assertListEqual(result, ['Auto Group'])
|
||||||
|
|
||||||
# actions
|
# actions
|
||||||
|
|
||||||
@ -539,6 +583,68 @@ class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
|||||||
self.assertNotIn(field, form.fields)
|
self.assertNotIn(field, form.fields)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
|
class TestGroupAdmin2(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.superuser = User.objects.create_superuser("super")
|
||||||
|
|
||||||
|
def test_should_remove_users_from_state_groups(self):
|
||||||
|
# given
|
||||||
|
user_member = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character_member = AuthUtils.add_main_character_2(
|
||||||
|
user_member,
|
||||||
|
name="Bruce Wayne",
|
||||||
|
character_id=1001,
|
||||||
|
corp_id=2001,
|
||||||
|
corp_name="Wayne Technologies",
|
||||||
|
)
|
||||||
|
user_guest = AuthUtils.create_user("Lex Luthor")
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
user_guest,
|
||||||
|
name="Lex Luthor",
|
||||||
|
character_id=1011,
|
||||||
|
corp_id=2011,
|
||||||
|
corp_name="Luthor Corp",
|
||||||
|
)
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
member_state.member_characters.add(character_member)
|
||||||
|
user_member.refresh_from_db()
|
||||||
|
user_guest.refresh_from_db()
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_member.groups.add(group)
|
||||||
|
user_guest.groups.add(group)
|
||||||
|
group.authgroup.states.add(member_state)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/groupmanagement/group/{group.pk}/change/",
|
||||||
|
data={
|
||||||
|
"name": f"{group.name}",
|
||||||
|
"authgroup-TOTAL_FORMS": "1",
|
||||||
|
"authgroup-INITIAL_FORMS": "1",
|
||||||
|
"authgroup-MIN_NUM_FORMS": "0",
|
||||||
|
"authgroup-MAX_NUM_FORMS": "1",
|
||||||
|
"authgroup-0-description": "",
|
||||||
|
"authgroup-0-states": f"{member_state.pk}",
|
||||||
|
"authgroup-0-internal": "on",
|
||||||
|
"authgroup-0-hidden": "on",
|
||||||
|
"authgroup-0-group": f"{group.pk}",
|
||||||
|
"authgroup-__prefix__-description": "",
|
||||||
|
"authgroup-__prefix__-internal": "on",
|
||||||
|
"authgroup-__prefix__-hidden": "on",
|
||||||
|
"authgroup-__prefix__-group": f"{group.pk}",
|
||||||
|
"_save": "Save"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/groupmanagement/group/")
|
||||||
|
self.assertIn(group, user_member.groups.all())
|
||||||
|
self.assertNotIn(group, user_guest.groups.all())
|
||||||
|
|
||||||
|
|
||||||
class TestReservedGroupNameAdmin(TestCase):
|
class TestReservedGroupNameAdmin(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
@ -232,6 +232,38 @@ class TestAuthGroup(TestCase):
|
|||||||
expected = 'Superheros'
|
expected = 'Superheros'
|
||||||
self.assertEqual(str(group.authgroup), expected)
|
self.assertEqual(str(group.authgroup), expected)
|
||||||
|
|
||||||
|
def test_should_remove_guests_from_group_when_restricted_to_members_only(self):
|
||||||
|
# given
|
||||||
|
user_member = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character_member = AuthUtils.add_main_character_2(
|
||||||
|
user_member,
|
||||||
|
name="Bruce Wayne",
|
||||||
|
character_id=1001,
|
||||||
|
corp_id=2001,
|
||||||
|
corp_name="Wayne Technologies",
|
||||||
|
)
|
||||||
|
user_guest = AuthUtils.create_user("Lex Luthor")
|
||||||
|
AuthUtils.add_main_character_2(
|
||||||
|
user_guest,
|
||||||
|
name="Lex Luthor",
|
||||||
|
character_id=1011,
|
||||||
|
corp_id=2011,
|
||||||
|
corp_name="Luthor Corp",
|
||||||
|
)
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
member_state.member_characters.add(character_member)
|
||||||
|
user_member.refresh_from_db()
|
||||||
|
user_guest.refresh_from_db()
|
||||||
|
group = Group.objects.create(name="dummy")
|
||||||
|
user_member.groups.add(group)
|
||||||
|
user_guest.groups.add(group)
|
||||||
|
group.authgroup.states.add(member_state)
|
||||||
|
# when
|
||||||
|
group.authgroup.remove_users_not_matching_states()
|
||||||
|
# then
|
||||||
|
self.assertIn(group, user_member.groups.all())
|
||||||
|
self.assertNotIn(group, user_guest.groups.all())
|
||||||
|
|
||||||
|
|
||||||
class TestAuthGroupRequestApprovers(TestCase):
|
class TestAuthGroupRequestApprovers(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
@ -8,7 +9,7 @@ from django.db.models.signals import pre_delete
|
|||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from .hooks import ServicesHook
|
from .hooks import ServicesHook
|
||||||
from .tasks import disable_user
|
from .tasks import disable_user, update_groups_for_user
|
||||||
|
|
||||||
from allianceauth.authentication.models import State, UserProfile
|
from allianceauth.authentication.models import State, UserProfile
|
||||||
from allianceauth.authentication.signals import state_changed
|
from allianceauth.authentication.signals import state_changed
|
||||||
@ -19,21 +20,27 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@receiver(m2m_changed, sender=User.groups.through)
|
@receiver(m2m_changed, sender=User.groups.through)
|
||||||
def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
|
def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
|
||||||
logger.debug(f"Received m2m_changed from {instance} groups with action {action}")
|
logger.debug(
|
||||||
|
"%s: Received m2m_changed from groups with action %s", instance, action
|
||||||
def trigger_service_group_update():
|
)
|
||||||
logger.debug("Triggering service group update for %s" % instance)
|
if instance.pk and (
|
||||||
# Iterate through Service hooks
|
action == "post_add" or action == "post_remove" or action == "post_clear"
|
||||||
for svc in ServicesHook.get_services():
|
):
|
||||||
try:
|
if isinstance(instance, User):
|
||||||
svc.validate_user(instance)
|
logger.debug(
|
||||||
svc.update_groups(instance)
|
"Waiting for commit to trigger service group update for %s", instance
|
||||||
except:
|
)
|
||||||
logger.exception(f'Exception running update_groups for services module {svc} on user {instance}')
|
transaction.on_commit(partial(update_groups_for_user.delay, instance.pk))
|
||||||
|
elif (
|
||||||
if instance.pk and (action == "post_add" or action == "post_remove" or action == "post_clear"):
|
isinstance(instance, Group)
|
||||||
logger.debug("Waiting for commit to trigger service group update for %s" % instance)
|
and kwargs.get("model") is User
|
||||||
transaction.on_commit(trigger_service_group_update)
|
and "pk_set" in kwargs
|
||||||
|
):
|
||||||
|
for user_pk in kwargs["pk_set"]:
|
||||||
|
logger.debug(
|
||||||
|
"%s: Waiting for commit to trigger service group update for user", user_pk
|
||||||
|
)
|
||||||
|
transaction.on_commit(partial(update_groups_for_user.delay, user_pk))
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=User.user_permissions.through)
|
@receiver(m2m_changed, sender=User.user_permissions.through)
|
||||||
|
@ -47,3 +47,20 @@ def disable_user(user):
|
|||||||
for svc in ServicesHook.get_services():
|
for svc in ServicesHook.get_services():
|
||||||
if svc.service_active_for_user(user):
|
if svc.service_active_for_user(user):
|
||||||
svc.delete_user(user)
|
svc.delete_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def update_groups_for_user(user_pk: int) -> None:
|
||||||
|
"""Update groups for all services registered to a user."""
|
||||||
|
user = User.objects.get(pk=user_pk)
|
||||||
|
logger.debug("%s: Triggering service group update for user", user)
|
||||||
|
for svc in ServicesHook.get_services():
|
||||||
|
try:
|
||||||
|
svc.validate_user(user)
|
||||||
|
svc.update_groups(user)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
'Exception running update_groups for services module %s on user %s',
|
||||||
|
svc,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import override_settings, TestCase, TransactionTestCase
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
|
||||||
from allianceauth.authentication.models import State
|
from allianceauth.authentication.models import State
|
||||||
@ -9,6 +9,9 @@ from allianceauth.eveonline.models import EveCharacter
|
|||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.services.signals'
|
||||||
|
|
||||||
|
|
||||||
class ServicesSignalsTestCase(TestCase):
|
class ServicesSignalsTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
|
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
|
||||||
@ -17,17 +20,12 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
|
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.transaction')
|
@mock.patch(MODULE_PATH + '.transaction', spec=True)
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.update_groups_for_user', spec=True)
|
||||||
def test_m2m_changed_user_groups(self, services_hook, transaction):
|
def test_m2m_changed_user_groups(self, update_groups_for_user, transaction):
|
||||||
"""
|
"""
|
||||||
Test that update_groups hook function is called on user groups change
|
Test that update_groups hook function is called on user groups change
|
||||||
"""
|
"""
|
||||||
svc = mock.Mock()
|
|
||||||
svc.update_groups.return_value = None
|
|
||||||
svc.validate_user.return_value = None
|
|
||||||
|
|
||||||
services_hook.get_services.return_value = [svc]
|
|
||||||
|
|
||||||
# Overload transaction.on_commit so everything happens synchronously
|
# Overload transaction.on_commit so everything happens synchronously
|
||||||
transaction.on_commit = lambda fn: fn()
|
transaction.on_commit = lambda fn: fn()
|
||||||
@ -39,17 +37,11 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
self.member.save()
|
self.member.save()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertTrue(services_hook.get_services.called)
|
self.assertTrue(update_groups_for_user.delay.called)
|
||||||
|
args, _ = update_groups_for_user.delay.call_args
|
||||||
|
self.assertEqual(self.member.pk, args[0])
|
||||||
|
|
||||||
self.assertTrue(svc.update_groups.called)
|
@mock.patch(MODULE_PATH + '.disable_user')
|
||||||
args, kwargs = svc.update_groups.call_args
|
|
||||||
self.assertEqual(self.member, args[0])
|
|
||||||
|
|
||||||
self.assertTrue(svc.validate_user.called)
|
|
||||||
args, kwargs = svc.validate_user.call_args
|
|
||||||
self.assertEqual(self.member, args[0])
|
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.disable_user')
|
|
||||||
def test_pre_delete_user(self, disable_user):
|
def test_pre_delete_user(self, disable_user):
|
||||||
"""
|
"""
|
||||||
Test that disable_member is called when a user is deleted
|
Test that disable_member is called when a user is deleted
|
||||||
@ -60,7 +52,7 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = disable_user.call_args
|
args, kwargs = disable_user.call_args
|
||||||
self.assertEqual(self.none_user, args[0])
|
self.assertEqual(self.none_user, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.disable_user')
|
@mock.patch(MODULE_PATH + '.disable_user')
|
||||||
def test_pre_save_user_inactivation(self, disable_user):
|
def test_pre_save_user_inactivation(self, disable_user):
|
||||||
"""
|
"""
|
||||||
Test a user set inactive has disable_member called
|
Test a user set inactive has disable_member called
|
||||||
@ -72,7 +64,7 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = disable_user.call_args
|
args, kwargs = disable_user.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.disable_user')
|
@mock.patch(MODULE_PATH + '.disable_user')
|
||||||
def test_disable_services_on_loss_of_main_character(self, disable_user):
|
def test_disable_services_on_loss_of_main_character(self, disable_user):
|
||||||
"""
|
"""
|
||||||
Test a user set inactive has disable_member called
|
Test a user set inactive has disable_member called
|
||||||
@ -84,8 +76,8 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = disable_user.call_args
|
args, kwargs = disable_user.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.transaction')
|
@mock.patch(MODULE_PATH + '.transaction')
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
def test_m2m_changed_group_permissions(self, services_hook, transaction):
|
def test_m2m_changed_group_permissions(self, services_hook, transaction):
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
svc = mock.Mock()
|
svc = mock.Mock()
|
||||||
@ -116,8 +108,8 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = svc.validate_user.call_args
|
args, kwargs = svc.validate_user.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.transaction')
|
@mock.patch(MODULE_PATH + '.transaction')
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
def test_m2m_changed_user_permissions(self, services_hook, transaction):
|
def test_m2m_changed_user_permissions(self, services_hook, transaction):
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
svc = mock.Mock()
|
svc = mock.Mock()
|
||||||
@ -145,8 +137,8 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = svc.validate_user.call_args
|
args, kwargs = svc.validate_user.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.transaction')
|
@mock.patch(MODULE_PATH + '.transaction')
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
def test_m2m_changed_user_state_permissions(self, services_hook, transaction):
|
def test_m2m_changed_user_state_permissions(self, services_hook, transaction):
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
svc = mock.Mock()
|
svc = mock.Mock()
|
||||||
@ -180,7 +172,7 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = svc.validate_user.call_args
|
args, kwargs = svc.validate_user.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
def test_state_changed_services_validation_and_groups_update(self, services_hook):
|
def test_state_changed_services_validation_and_groups_update(self, services_hook):
|
||||||
"""Test a user changing state has service accounts validated and groups updated
|
"""Test a user changing state has service accounts validated and groups updated
|
||||||
"""
|
"""
|
||||||
@ -206,8 +198,7 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = svc.update_groups.call_args
|
args, kwargs = svc.update_groups.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
|
||||||
def test_state_changed_services_validation_and_groups_update_1(self, services_hook):
|
def test_state_changed_services_validation_and_groups_update_1(self, services_hook):
|
||||||
"""Test a user changing main has service accounts validated and sync updated
|
"""Test a user changing main has service accounts validated and sync updated
|
||||||
"""
|
"""
|
||||||
@ -238,7 +229,7 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
args, kwargs = svc.sync_nickname.call_args
|
args, kwargs = svc.sync_nickname.call_args
|
||||||
self.assertEqual(self.member, args[0])
|
self.assertEqual(self.member, args[0])
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.signals.ServicesHook')
|
@mock.patch(MODULE_PATH + '.ServicesHook')
|
||||||
def test_state_changed_services_validation_and_groups_update_2(self, services_hook):
|
def test_state_changed_services_validation_and_groups_update_2(self, services_hook):
|
||||||
"""Test a user changing main has service does not have accounts validated
|
"""Test a user changing main has service does not have accounts validated
|
||||||
and sync updated if the new main is equal to the old main
|
and sync updated if the new main is equal to the old main
|
||||||
@ -260,3 +251,71 @@ class ServicesSignalsTestCase(TestCase):
|
|||||||
self.assertFalse(services_hook.get_services.called)
|
self.assertFalse(services_hook.get_services.called)
|
||||||
self.assertFalse(svc.validate_user.called)
|
self.assertFalse(svc.validate_user.called)
|
||||||
self.assertFalse(svc.sync_nickname.called)
|
self.assertFalse(svc.sync_nickname.called)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
"allianceauth.services.modules.mumble.auth_hooks.MumbleService.update_groups"
|
||||||
|
)
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
|
class TestUserGroupBulkUpdate(TransactionTestCase):
|
||||||
|
def test_should_run_user_service_check_when_group_added_to_user(
|
||||||
|
self, mock_update_groups
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
group = Group.objects.create(name="Group")
|
||||||
|
mock_update_groups.reset_mock()
|
||||||
|
# when
|
||||||
|
user.groups.add(group)
|
||||||
|
# then
|
||||||
|
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
|
||||||
|
self.assertSetEqual(users_updated, {user})
|
||||||
|
|
||||||
|
def test_should_run_user_service_check_when_multiple_groups_are_added_to_user(
|
||||||
|
self, mock_update_groups
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
group_1 = Group.objects.create(name="Group 1")
|
||||||
|
group_2 = Group.objects.create(name="Group 2")
|
||||||
|
mock_update_groups.reset_mock()
|
||||||
|
# when
|
||||||
|
user.groups.add(group_1, group_2)
|
||||||
|
# then
|
||||||
|
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
|
||||||
|
self.assertSetEqual(users_updated, {user})
|
||||||
|
|
||||||
|
def test_should_run_user_service_check_when_user_added_to_group(
|
||||||
|
self, mock_update_groups
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
group = Group.objects.create(name="Group")
|
||||||
|
mock_update_groups.reset_mock()
|
||||||
|
# when
|
||||||
|
group.user_set.add(user)
|
||||||
|
# then
|
||||||
|
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
|
||||||
|
self.assertSetEqual(users_updated, {user})
|
||||||
|
|
||||||
|
def test_should_run_user_service_check_when_multiple_users_are_added_to_group(
|
||||||
|
self, mock_update_groups
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
user_1 = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
AuthUtils.add_main_character_2(user_1, "Bruce Wayne", 1001)
|
||||||
|
user_2 = AuthUtils.create_user("Peter Parker")
|
||||||
|
AuthUtils.add_main_character_2(user_2, "Peter Parker", 1002)
|
||||||
|
user_3 = AuthUtils.create_user("Lex Luthor")
|
||||||
|
AuthUtils.add_main_character_2(user_3, "Lex Luthor", 1011)
|
||||||
|
group = Group.objects.create(name="Group")
|
||||||
|
user_1.groups.add(group)
|
||||||
|
mock_update_groups.reset_mock()
|
||||||
|
# when
|
||||||
|
group.user_set.add(user_2, user_3)
|
||||||
|
# then
|
||||||
|
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
|
||||||
|
self.assertSetEqual(users_updated, {user_2, user_3})
|
||||||
|
@ -3,32 +3,50 @@ from unittest import mock
|
|||||||
from celery_once import AlreadyQueued
|
from celery_once import AlreadyQueued
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import override_settings, TestCase
|
||||||
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
from allianceauth.services.tasks import validate_services
|
from allianceauth.services.tasks import validate_services, update_groups_for_user
|
||||||
|
|
||||||
from ..tasks import DjangoBackend
|
from ..tasks import DjangoBackend
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
class ServicesTasksTestCase(TestCase):
|
class ServicesTasksTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.member = AuthUtils.create_user('auth_member')
|
self.member = AuthUtils.create_user('auth_member')
|
||||||
|
|
||||||
@mock.patch('allianceauth.services.tasks.ServicesHook')
|
@mock.patch('allianceauth.services.tasks.ServicesHook')
|
||||||
def test_validate_services(self, services_hook):
|
def test_validate_services(self, services_hook):
|
||||||
|
# given
|
||||||
svc = mock.Mock()
|
svc = mock.Mock()
|
||||||
svc.validate_user.return_value = None
|
svc.validate_user.return_value = None
|
||||||
|
|
||||||
services_hook.get_services.return_value = [svc]
|
services_hook.get_services.return_value = [svc]
|
||||||
|
# when
|
||||||
validate_services.delay(self.member.pk)
|
validate_services.delay(self.member.pk)
|
||||||
|
# then
|
||||||
self.assertTrue(services_hook.get_services.called)
|
self.assertTrue(services_hook.get_services.called)
|
||||||
self.assertTrue(svc.validate_user.called)
|
self.assertTrue(svc.validate_user.called)
|
||||||
args, kwargs = svc.validate_user.call_args
|
args, _ = svc.validate_user.call_args
|
||||||
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
|
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
|
||||||
|
|
||||||
|
@mock.patch('allianceauth.services.tasks.ServicesHook')
|
||||||
|
def test_update_groups_for_user(self, services_hook):
|
||||||
|
# given
|
||||||
|
svc = mock.Mock()
|
||||||
|
svc.validate_user.return_value = None
|
||||||
|
services_hook.get_services.return_value = [svc]
|
||||||
|
# when
|
||||||
|
update_groups_for_user.delay(self.member.pk)
|
||||||
|
# then
|
||||||
|
self.assertTrue(services_hook.get_services.called)
|
||||||
|
self.assertTrue(svc.validate_user.called)
|
||||||
|
args, _ = svc.validate_user.call_args
|
||||||
|
self.assertEqual(self.member, args[0]) # Assert correct user
|
||||||
|
self.assertTrue(svc.update_groups.called)
|
||||||
|
args, _ = svc.update_groups.call_args
|
||||||
|
self.assertEqual(self.member, args[0]) # Assert correct user
|
||||||
|
|
||||||
|
|
||||||
class TestDjangoBackend(TestCase):
|
class TestDjangoBackend(TestCase):
|
||||||
|
|
||||||
|
114
docs/_static/css/rtd_dark.css
vendored
Normal file
114
docs/_static/css/rtd_dark.css
vendored
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/*!
|
||||||
|
* @name Readthedocs
|
||||||
|
* @namespace http://userstyles.org
|
||||||
|
* @description Styles the documentation pages hosted on Readthedocs.io
|
||||||
|
* @author Anthony Post
|
||||||
|
* @homepage https://userstyles.org/styles/142968
|
||||||
|
* @version 0.20170529055029
|
||||||
|
*
|
||||||
|
* Modified by Aloïs Dreyfus: 20200527-1037
|
||||||
|
* Modified by Erik Kalkoken: 20220615
|
||||||
|
*/
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
a:visited {
|
||||||
|
color: #bf84d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-nav-content {
|
||||||
|
background: #3c3c3c;
|
||||||
|
color: aliceblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method dt, .class dt, .data dt, .attribute dt, .function dt,
|
||||||
|
.descclassname, .descname {
|
||||||
|
background-color: #525252 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-backref {
|
||||||
|
color: grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.literal {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
border: 1px solid #6d6d6d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-nav-content-wrap {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: #191919 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
background-color: #2b2b2b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xref, .py-meth {
|
||||||
|
color: #7ec3e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admonition, .note {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-side-nav-search {
|
||||||
|
background-color: inherit;
|
||||||
|
border-bottom: 1px solid #fcfcfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead {
|
||||||
|
background-color: #b9b9b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th {
|
||||||
|
border: solid 2px #e1e4e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-table thead p, .rst-content table.docutils thead p, .rst-content table.field-list thead p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {
|
||||||
|
background-color: #343131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight .m {
|
||||||
|
color: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Literal.Number */
|
||||||
|
.highlight .nv {
|
||||||
|
color: #3a7ca8
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Name.Variable */
|
||||||
|
|
||||||
|
body {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rst-content .section .admonition ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.toctree-l1 {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-menu-vertical li code {
|
||||||
|
color: #E74C3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-menu-vertical .xref {
|
||||||
|
color: #2980B9 !important;
|
||||||
|
}
|
||||||
|
}
|
@ -111,6 +111,7 @@ html_theme_options = {
|
|||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
html_css_files = ["css/rtd_dark.css"]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ------------------------------------------
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
@ -150,12 +150,14 @@ sudo redis-server --daemonize yes
|
|||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. note::
|
.. note::
|
||||||
WSL does not have an init.d service, so it will not automatically start your services such as MySQL and Redis when you boot your Windows machine. For convenience we recommend putting the commands for starting these services in a bash script. Here is an example: ::
|
WSL does not have an init.d service, so it will not automatically start your services such as MySQL and Redis when you boot your Windows machine. For convenience we recommend putting the commands for starting these services in a bash script. Here is an example:
|
||||||
|
|
||||||
#/bin/bash
|
::
|
||||||
# start services for AA dev
|
|
||||||
sudo service mysql start
|
#/bin/bash
|
||||||
sudo redis-server --daemonize yes
|
# start services for AA dev
|
||||||
|
sudo service mysql start
|
||||||
|
sudo redis-server --daemonize yes
|
||||||
|
|
||||||
In addition it is possible to configure Windows to automatically start WSL services, but that procedure goes beyond the scopes of this guide.
|
In addition it is possible to configure Windows to automatically start WSL services, but that procedure goes beyond the scopes of this guide.
|
||||||
```
|
```
|
||||||
|
@ -1,15 +1,27 @@
|
|||||||
=============
|
=======================
|
||||||
Template Tags
|
Template tags & filters
|
||||||
=============
|
=======================
|
||||||
|
|
||||||
The following template tags are available to be used by all apps. To use them just load the respeetive template tag in your template like so:
|
The following template tags and filters are available to be used by all apps. To use them just load them into your template like so:
|
||||||
|
|
||||||
.. code-block:: html
|
.. code-block:: html+django
|
||||||
|
|
||||||
{% load evelinks %}
|
{% load evelinks %}
|
||||||
|
|
||||||
|
|
||||||
|
Template Filters
|
||||||
|
================
|
||||||
|
|
||||||
evelinks
|
evelinks
|
||||||
========
|
--------
|
||||||
|
|
||||||
|
Example for using an evelinks filter to render an alliance logo:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
<img src="{{ alliance_id|alliance_logo_url }}">
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: allianceauth.eveonline.templatetags.evelinks
|
.. automodule:: allianceauth.eveonline.templatetags.evelinks
|
||||||
:members:
|
:members:
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
sphinx>=4.4.0,<5.0.0
|
sphinx>=4.4.0,<5.0.0
|
||||||
sphinx_rtd_theme>=1.0.0,<2.0.0
|
sphinx_rtd_theme>=1.0.0,<2.0.0
|
||||||
recommonmark==0.7.1
|
recommonmark==0.7.1
|
||||||
|
Jinja2<3.1
|
||||||
|
|
||||||
# Autodoc dependencies
|
# Autodoc dependencies
|
||||||
django>=4.0.2,<5.0.0
|
django>=4.0.2,<5.0.0
|
||||||
django-celery-beat>=2.0.0
|
django-celery-beat>=2.0.0
|
||||||
|
django-redis-cache
|
||||||
django-bootstrap-form
|
django-bootstrap-form
|
||||||
django-sortedm2m
|
django-sortedm2m
|
||||||
django-esi>=4.0.0a1,<5
|
django-esi>=4.0.0a1,<5
|
||||||
django-redis>=5.2.0<6.0.0
|
|
||||||
celery>=5.2.0,<6.0.0
|
celery>=5.2.0,<6.0.0
|
||||||
celery_once>=3.0.1
|
celery_once>=3.0.1
|
||||||
passlib
|
passlib
|
||||||
|
10
tox.ini
10
tox.ini
@ -2,7 +2,7 @@
|
|||||||
isolated_build = True
|
isolated_build = True
|
||||||
skipsdist = true
|
skipsdist = true
|
||||||
usedevelop = true
|
usedevelop = true
|
||||||
envlist = py{38,39,310,311}-{all,core}
|
envlist = py{38,39,310,311}-{all,core}, docs
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv =
|
setenv =
|
||||||
@ -22,3 +22,11 @@ commands =
|
|||||||
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
|
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
|
||||||
all: coverage report -m
|
all: coverage report -m
|
||||||
all: coverage xml
|
all: coverage xml
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
description = invoke sphinx-build to build the HTML docs
|
||||||
|
basepython = python3.9
|
||||||
|
deps = -r{toxinidir}/docs/requirements.txt
|
||||||
|
install_command =
|
||||||
|
commands =
|
||||||
|
sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user