diff --git a/allianceauth/groupmanagement/managers.py b/allianceauth/groupmanagement/managers.py
index d85771dd..ad8db92e 100644
--- a/allianceauth/groupmanagement/managers.py
+++ b/allianceauth/groupmanagement/managers.py
@@ -14,8 +14,8 @@ class GroupManager:
@classmethod
def get_joinable_groups_for_user(
- cls, user: User, include_hidden = True
- ) -> QuerySet:
+ cls, user: User, include_hidden=True
+ ) -> QuerySet[Group]:
"""get groups a user could join incl. groups already joined"""
groups_qs = cls.get_joinable_groups(user.profile.state)
@@ -28,24 +28,27 @@ class GroupManager:
return groups_qs
@staticmethod
- def get_joinable_groups(state: State) -> QuerySet:
+ def get_joinable_groups(state: State) -> QuerySet[Group]:
"""get groups that can be joined by user with given state"""
- return Group.objects\
- .select_related('authgroup')\
- .exclude(authgroup__internal=True)\
- .filter(Q(authgroup__states=state) | Q(authgroup__states=None))
-
- @staticmethod
- def get_all_non_internal_groups() -> QuerySet:
- """get groups that are not internal"""
- return Group.objects\
- .select_related('authgroup')\
+ return (
+ Group.objects
.exclude(authgroup__internal=True)
+ .filter(Q(authgroup__states=state) | Q(authgroup__states=None))
+ )
@staticmethod
- def get_group_leaders_groups(user: User):
- return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \
- Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all())
+ def get_all_non_internal_groups() -> QuerySet[Group]:
+ """get groups that are not internal"""
+ return Group.objects.exclude(authgroup__internal=True)
+
+ @staticmethod
+ def get_group_leaders_groups(user: User) -> QuerySet[Group]:
+ return (
+ Group.objects.filter(authgroup__group_leaders=user)
+ | Group.objects.filter(
+ authgroup__group_leader_groups__in=list(user.groups.all())
+ )
+ )
@staticmethod
def joinable_group(group: Group, state: State) -> bool:
@@ -57,12 +60,12 @@ class GroupManager:
:param state: allianceauth.authentication.State object
:return: bool True if its joinable, False otherwise
"""
- if (len(group.authgroup.states.all()) != 0
+ if (
+ len(group.authgroup.states.all()) != 0
and state not in group.authgroup.states.all()
):
return False
- else:
- return not group.authgroup.internal
+ return not group.authgroup.internal
@staticmethod
def check_internal_group(group: Group) -> bool:
@@ -78,7 +81,7 @@ class GroupManager:
return user.has_perm('auth.group_management')
@classmethod
- def can_manage_groups(cls, user:User ) -> bool:
+ def can_manage_groups(cls, user:User) -> bool:
"""
For use with user_passes_test decorator.
Check if the user can manage groups. Either has the
@@ -88,7 +91,10 @@ class GroupManager:
:return: bool True if user can manage groups, False otherwise
"""
if user.is_authenticated:
- return cls.has_management_permission(user) or cls.get_group_leaders_groups(user)
+ return (
+ cls.has_management_permission(user)
+ or cls.get_group_leaders_groups(user)
+ )
return False
@classmethod
@@ -100,19 +106,19 @@ class GroupManager:
:return: True if the user can manage the group
"""
if user.is_authenticated:
- return cls.has_management_permission(user) or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
+ return (
+ cls.has_management_permission(user)
+ or cls.get_group_leaders_groups(user).filter(pk=group.pk).exists()
+ )
return False
@classmethod
def pending_requests_count_for_user(cls, user: User) -> int:
"""Returns the number of pending group requests for the given user"""
-
if cls.has_management_permission(user):
return GroupRequest.objects.all().count()
- else:
- return (
- GroupRequest.objects
- .filter(group__authgroup__group_leaders__exact=user)
- .select_related("group__authgroup__group_leaders")
- .count()
- )
+ return (
+ GroupRequest.objects
+ .filter(group__in=list(cls.get_group_leaders_groups(user)))
+ .count()
+ )
diff --git a/allianceauth/groupmanagement/migrations/0017_improve_groups_documentation.py b/allianceauth/groupmanagement/migrations/0017_improve_groups_documentation.py
new file mode 100644
index 00000000..f59adf18
--- /dev/null
+++ b/allianceauth/groupmanagement/migrations/0017_improve_groups_documentation.py
@@ -0,0 +1,42 @@
+# Generated by Django 3.2.9 on 2021-11-11 15:56
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ('authentication', '0019_merge_20211026_0919'),
+ ('groupmanagement', '0016_remove_grouprequest_status_field'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='authgroup',
+ name='group_leader_groups',
+ field=models.ManyToManyField(blank=True, help_text='Members of leader groups can process requests for this group. Use the auth.group_management
permission to allow a user to manage all groups.
', related_name='leads_group_groups', to='auth.Group'),
+ ),
+ migrations.AlterField(
+ model_name='authgroup',
+ name='group_leaders',
+ field=models.ManyToManyField(blank=True, help_text='Group leaders can process requests for this group. Use the auth.group_management
permission to allow a user to manage all groups.
', related_name='leads_groups', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='authgroup',
+ name='open',
+ field=models.BooleanField(default=False, help_text='Group is open and users will be automatically added upon request.
If the group is not open users will need their request manually approved.'),
+ ),
+ migrations.AlterField(
+ model_name='authgroup',
+ name='public',
+ field=models.BooleanField(default=False, help_text='Group is public. Any registered user is able to join this group, with visibility based on the other options set for this group.
Auth will not remove users from this group automatically when they are no longer authenticated.'),
+ ),
+ migrations.AlterField(
+ model_name='authgroup',
+ name='states',
+ field=models.ManyToManyField(blank=True, help_text='States listed here will have the ability to join this group provided they have the proper permissions.
', related_name='valid_states', to='authentication.State'),
+ ),
+ ]
diff --git a/allianceauth/groupmanagement/models.py b/allianceauth/groupmanagement/models.py
index 0959ccb1..c023c643 100644
--- a/allianceauth/groupmanagement/models.py
+++ b/allianceauth/groupmanagement/models.py
@@ -1,16 +1,26 @@
+from typing import Set
+
+from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
+from django.utils.translation import gettext_lazy as _
+
from allianceauth.authentication.models import State
+from allianceauth.notifications import notify
class GroupRequest(models.Model):
+ """Request from a user for joining or leaving a group."""
leave_request = models.BooleanField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
+ def __str__(self):
+ return self.user.username + ":" + self.group.name
+
@property
def main_char(self):
"""
@@ -19,11 +29,22 @@ class GroupRequest(models.Model):
"""
return self.user.profile.main_character
- def __str__(self):
- return self.user.username + ":" + self.group.name
+ def notify_leaders(self) -> None:
+ """Send notification to all group leaders about this request.
+
+ Note: No translations, because language for each leader is unknown
+ """
+ if not getattr(settings, 'GROUPMANAGEMENT_REQUESTS_NOTIFICATION', False):
+ return
+ keyword = "leave" if self.leave_request else "join"
+ title = f"Group Management: {keyword.title()} request for {self.group.name}"
+ message = f"{self.user} want's to {keyword} {self.group.name}."
+ for appover in self.group.authgroup.group_request_approvers():
+ notify(user=appover, title=title, message=message, level="info")
class RequestLog(models.Model):
+ """Log entry about who joined and left a group and who approved it."""
request_type = models.BooleanField(null=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
request_info = models.CharField(max_length=254)
@@ -61,11 +82,12 @@ class AuthGroup(models.Model):
e.g. group.authgroup.internal
Logic:
- Internal - not requestable by users, at all. Covers Corp_, Alliance_, Members etc groups.
- Groups are internal by default
+ Internal - not requestable by users, at all. Covers Corp_, Alliance_,
+ Members etc groups. Groups are internal by default
- Public - Other options are respected, but any user will be able to become and remain a member, even if they
- have no API etc entered. Auth will not manage these groups automatically so user removal is up to
+ Public - Other options are respected, but any user will be able to become
+ and remain a member, even if they have no API etc entered.
+ Auth will not manage these groups automatically so user removal is up to
group managers/leaders.
Not Internal and:
@@ -75,46 +97,88 @@ class AuthGroup(models.Model):
Not Open - Users requests must be approved before they are added to the group
"""
group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
+ internal = models.BooleanField(
+ default=True,
+ help_text=_(
+ "Internal group, users cannot see, join or request to join this group.
"
+ "Used for groups such as Members, Corp_*, Alliance_* etc.
"
+ "Overrides Hidden and Open options when selected."
+ )
+ )
+ hidden = models.BooleanField(
+ default=True,
+ help_text=_(
+ "Group is hidden from users but can still join with the correct link."
+ )
+ )
+ open = models.BooleanField(
+ default=False,
+ help_text=_(
+ "Group is open and users will be automatically added upon request.
"
+ "If the group is not open users will need their request manually approved."
+ )
+ )
+ public = models.BooleanField(
+ default=False,
+ help_text=_(
+ "Group is public. Any registered user is able to join this group, with "
+ "visibility based on the other options set for this group.
"
+ "Auth will not remove users from this group automatically when they "
+ "are no longer authenticated."
+ )
+ )
+ group_leaders = models.ManyToManyField(
+ User,
+ related_name='leads_groups',
+ blank=True,
+ help_text=_(
+ "Group leaders can process requests for this group. "
+ "Use the auth.group_management
permission to allow "
+ "a user to manage all groups.
"
+ )
+ )
+ group_leader_groups = models.ManyToManyField(
+ Group,
+ related_name='leads_group_groups',
+ blank=True,
+ help_text=_(
+ "Members of leader groups can process requests for this group. "
+ "Use the auth.group_management
permission "
+ "to allow a user to manage all groups.
")
+ )
+ states = models.ManyToManyField(
+ State,
+ related_name='valid_states',
+ blank=True,
+ help_text=_(
+ "States listed here will have the ability to join this group provided "
+ "they have the proper permissions.
"
+ )
+ )
+ description = models.TextField(
+ max_length=512,
+ blank=True,
+ help_text=_(
+ "Short description (max. 512 characters) "
+ "of the group shown to users."
+ )
+ )
- internal = models.BooleanField(default=True,
- help_text="Internal group, users cannot see, join or request to join this group.
"
- "Used for groups such as Members, Corp_*, Alliance_* etc.
"
- "Overrides Hidden and Open options when selected.")
- hidden = models.BooleanField(default=True, help_text="Group is hidden from users but can still join with the correct link.")
- open = models.BooleanField(default=False,
- help_text="Group is open and users will be automatically added upon request.
"
- "If the group is not open users will need their request manually approved.")
- public = models.BooleanField(default=False,
- help_text="Group is public. Any registered user is able to join this group, with "
- "visibility based on the other options set for this group.
Auth will "
- "not remove users from this group automatically when they are no longer "
- "authenticated.")
- # Group leaders have management access to this group
- group_leaders = models.ManyToManyField(User, related_name='leads_groups', blank=True,
- help_text="Group leaders can process group requests for this group "
- "specifically. Use the auth.group_management permission to allow "
- "a user to manage all groups.")
- # allow groups to be *group leads*
- group_leader_groups = models.ManyToManyField(Group, related_name='leads_group_groups', blank=True,
- help_text="Group leaders can process group requests for this group "
- "specifically. Use the auth.group_management permission to allow "
- "a user to manage all groups.")
-
- states = models.ManyToManyField(State, related_name='valid_states', blank=True,
- help_text="States listed here will have the ability to join this group provided "
- "they have the proper permissions.")
-
- description = models.TextField(max_length=512, blank=True, help_text="Short description (max. 512 characters)"
- " of the group shown to users.")
+ class Meta:
+ permissions = (
+ ("request_groups", _("Can request non-public groups")),
+ )
+ default_permissions = tuple()
def __str__(self):
return self.group.name
- class Meta:
- permissions = (
- ("request_groups", "Can request non-public groups"),
+ def group_request_approvers(self) -> Set[User]:
+ """Return all users who can approve a group request."""
+ return set(
+ self.group_leaders.all()
+ | User.objects.filter(groups__in=list(self.group_leader_groups.all()))
)
- default_permissions = tuple()
@receiver(post_save, sender=Group)
diff --git a/allianceauth/groupmanagement/tests/test_managers.py b/allianceauth/groupmanagement/tests/test_managers.py
index c8f9ae53..f656d99e 100644
--- a/allianceauth/groupmanagement/tests/test_managers.py
+++ b/allianceauth/groupmanagement/tests/test_managers.py
@@ -1,8 +1,5 @@
-from unittest.mock import Mock, patch
-
from django.contrib.auth.models import Group, User
from django.test import TestCase
-from django.urls import reverse
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils
@@ -44,9 +41,9 @@ class GroupManagementVisibilityTestCase(TestCase):
self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user)
- self.assertIn(self.group1, groups) #avail due to user
- self.assertNotIn(self.group2, groups) #not avail due to group
- self.assertNotIn(self.group3, groups) #not avail at all
+ self.assertIn(self.group1, groups) # avail due to user
+ self.assertNotIn(self.group2, groups) # not avail due to group
+ self.assertNotIn(self.group3, groups) # not avail at all
self.user.groups.add(self.group1)
self._refresh_user()
@@ -71,70 +68,66 @@ class GroupManagementVisibilityTestCase(TestCase):
class TestGroupManager(TestCase):
-
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
-
+ def setUp(self) -> None:
# group 1
- cls.group_default = Group.objects.create(name='default')
- cls.group_default.authgroup.description = 'Default Group'
- cls.group_default.authgroup.internal = False
- cls.group_default.authgroup.hidden = False
- cls.group_default.authgroup.save()
+ self.group_default = Group.objects.create(name='default')
+ self.group_default.authgroup.description = 'Default Group'
+ self.group_default.authgroup.internal = False
+ self.group_default.authgroup.hidden = False
+ self.group_default.authgroup.save()
# group 2
- cls.group_internal = Group.objects.create(name='internal')
- cls.group_internal.authgroup.description = 'Internal Group'
- cls.group_internal.authgroup.internal = True
- cls.group_internal.authgroup.save()
+ self.group_internal = Group.objects.create(name='internal')
+ self.group_internal.authgroup.description = 'Internal Group'
+ self.group_internal.authgroup.internal = True
+ self.group_internal.authgroup.save()
# group 3
- cls.group_hidden = Group.objects.create(name='hidden')
- cls.group_hidden.authgroup.description = 'Hidden Group'
- cls.group_hidden.authgroup.internal = False
- cls.group_hidden.authgroup.hidden = True
- cls.group_hidden.authgroup.save()
+ self.group_hidden = Group.objects.create(name='hidden')
+ self.group_hidden.authgroup.description = 'Hidden Group'
+ self.group_hidden.authgroup.internal = False
+ self.group_hidden.authgroup.hidden = True
+ self.group_hidden.authgroup.save()
# group 4
- cls.group_open = Group.objects.create(name='open')
- cls.group_open.authgroup.description = 'Open Group'
- cls.group_open.authgroup.internal = False
- cls.group_open.authgroup.hidden = False
- cls.group_open.authgroup.open = True
- cls.group_open.authgroup.save()
+ self.group_open = Group.objects.create(name='open')
+ self.group_open.authgroup.description = 'Open Group'
+ self.group_open.authgroup.internal = False
+ self.group_open.authgroup.hidden = False
+ self.group_open.authgroup.open = True
+ self.group_open.authgroup.save()
# group 5
- cls.group_public_1 = Group.objects.create(name='public 1')
- cls.group_public_1.authgroup.description = 'Public Group 1'
- cls.group_public_1.authgroup.internal = False
- cls.group_public_1.authgroup.hidden = False
- cls.group_public_1.authgroup.public = True
- cls.group_public_1.authgroup.save()
+ self.group_public_1 = Group.objects.create(name='public 1')
+ self.group_public_1.authgroup.description = 'Public Group 1'
+ self.group_public_1.authgroup.internal = False
+ self.group_public_1.authgroup.hidden = False
+ self.group_public_1.authgroup.public = True
+ self.group_public_1.authgroup.save()
# group 6
- cls.group_public_2 = Group.objects.create(name='public 2')
- cls.group_public_2.authgroup.description = 'Public Group 2'
- cls.group_public_2.authgroup.internal = False
- cls.group_public_2.authgroup.hidden = True
- cls.group_public_2.authgroup.open = True
- cls.group_public_2.authgroup.public = True
- cls.group_public_2.authgroup.save()
+ self.group_public_2 = Group.objects.create(name='public 2')
+ self.group_public_2.authgroup.description = 'Public Group 2'
+ self.group_public_2.authgroup.internal = False
+ self.group_public_2.authgroup.hidden = True
+ self.group_public_2.authgroup.open = True
+ self.group_public_2.authgroup.public = True
+ self.group_public_2.authgroup.save()
# group 7
- cls.group_default_member = Group.objects.create(name='default members')
- cls.group_default_member.authgroup.description = \
+ self.group_default_member = Group.objects.create(name='default members')
+ self.group_default_member.authgroup.description = \
'Default Group for members only'
- cls.group_default_member.authgroup.internal = False
- cls.group_default_member.authgroup.hidden = False
- cls.group_default_member.authgroup.open = False
- cls.group_default_member.authgroup.public = False
- cls.group_default_member.authgroup.states.add(
+ self.group_default_member.authgroup.internal = False
+ self.group_default_member.authgroup.hidden = False
+ self.group_default_member.authgroup.open = False
+ self.group_default_member.authgroup.public = False
+ self.group_default_member.authgroup.states.add(
AuthUtils.get_member_state()
)
- cls.group_default_member.authgroup.save()
+ self.group_default_member.authgroup.save()
- def setUp(self):
+ # user
self.user = AuthUtils.create_user('Bruce Wayne')
def test_get_joinable_group_member(self):
@@ -241,7 +234,7 @@ class TestGroupManager(TestCase):
def test_get_joinable_groups_for_user_member_w_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
- AuthUtils.add_permission_to_user_by_name(
+ self.user = AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user
)
result = GroupManager.get_joinable_groups_for_user(self.user)
@@ -257,7 +250,7 @@ class TestGroupManager(TestCase):
def test_get_joinable_groups_for_user_member_w_permission_no_hidden(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True)
- AuthUtils.add_permission_to_user_by_name(
+ self.user = AuthUtils.add_permission_to_user_by_name(
'groupmanagement.request_groups', self.user
)
result = GroupManager.get_joinable_groups_for_user(
@@ -273,7 +266,7 @@ class TestGroupManager(TestCase):
def test_has_management_permission(self):
user = AuthUtils.create_user('Clark Kent')
- AuthUtils.add_permission_to_user_by_name(
+ user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(GroupManager.has_management_permission(user))
@@ -288,7 +281,7 @@ class TestGroupManager(TestCase):
def test_can_manage_groups_has_perm(self):
user = AuthUtils.create_user('Clark Kent')
- AuthUtils.add_permission_to_user_by_name(
+ user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(GroupManager.can_manage_groups(user))
@@ -306,7 +299,7 @@ class TestGroupManager(TestCase):
def test_can_manage_group_has_perm(self):
user = AuthUtils.create_user('Clark Kent')
- AuthUtils.add_permission_to_user_by_name(
+ user = AuthUtils.add_permission_to_user_by_name(
'auth.group_management', user
)
self.assertTrue(
@@ -433,11 +426,21 @@ class TestPendingRequestsCountForUser(TestCase):
# when user_requestor is requesting access to group 1
# then return 1 for user_leader_4
user_leader_4 = AuthUtils.create_member("Lex Luther")
- AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4)
- user_leader_4 = User.objects.get(pk=user_leader_4.pk)
- GroupRequest.objects.create(
- user=self.user_requestor, group=self.group_1
+ user_leader_4 = AuthUtils.add_permission_to_user_by_name(
+ "auth.group_management", user_leader_4
)
+ GroupRequest.objects.create(user=self.user_requestor, group=self.group_1)
self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1
)
+
+ def test_single_request_for_members_of_leading_group(self):
+ # given
+ leader_group = Group.objects.create(name="Leaders")
+ self.group_3.authgroup.group_leader_groups.add(leader_group)
+ self.user_leader_1.groups.add(leader_group)
+ GroupRequest.objects.create(user=self.user_requestor, group=self.group_3)
+ # when
+ result = GroupManager.pending_requests_count_for_user(self.user_leader_1)
+ # then
+ self.assertEqual(result, 1)
diff --git a/allianceauth/groupmanagement/tests/test_models.py b/allianceauth/groupmanagement/tests/test_models.py
index d3737e4f..b8179550 100644
--- a/allianceauth/groupmanagement/tests/test_models.py
+++ b/allianceauth/groupmanagement/tests/test_models.py
@@ -1,31 +1,22 @@
from unittest import mock
-from django.contrib.auth.models import User, Group
-from django.test import TestCase
+from django.contrib.auth.models import Group
+from django.test import TestCase, override_settings
from allianceauth.tests.auth_utils import AuthUtils
-from allianceauth.eveonline.models import (
- EveCorporationInfo, EveAllianceInfo, EveCharacter
-)
from ..models import GroupRequest, RequestLog
+MODULE_PATH = "allianceauth.groupmanagement.models"
+
def create_testdata():
- # clear DB
- User.objects.all().delete()
- Group.objects.all().delete()
- EveCharacter.objects.all().delete()
- EveCorporationInfo.objects.all().delete()
- EveAllianceInfo.objects.all().delete()
-
# group 1
group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group'
group.authgroup.internal = False
group.authgroup.hidden = False
group.authgroup.save()
-
# user 1
user_1 = AuthUtils.create_user('Bruce Wayne')
AuthUtils.add_main_character_2(
@@ -37,7 +28,6 @@ def create_testdata():
)
user_1.groups.add(group)
group.authgroup.group_leaders.add(user_1)
-
# user 2
user_2 = AuthUtils.create_user('Clark Kent')
AuthUtils.add_main_character_2(
@@ -45,18 +35,25 @@ def create_testdata():
name='Clark Kent',
character_id=1002,
corp_id=2002,
- corp_name='Wayne Technologies'
+ corp_name='Wayne Food'
)
- return group, user_1, user_2
-
+ # user 3
+ user_3 = AuthUtils.create_user('Peter Parker')
+ AuthUtils.add_main_character_2(
+ user_2,
+ name='Peter Parker',
+ character_id=1003,
+ corp_id=2002,
+ corp_name='Wayne Food'
+ )
+ return group, user_1, user_2, user_3
class TestGroupRequest(TestCase):
-
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.group, cls.user_1, _ = create_testdata()
+ cls.group, cls.user_1, cls.user_2, cls.user_3 = create_testdata()
def test_main_char(self):
group_request = GroupRequest.objects.create(
@@ -74,13 +71,85 @@ class TestGroupRequest(TestCase):
expected = 'Bruce Wayne:Superheros'
self.assertEqual(str(group_request), expected)
+ @override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
+ def test_should_notify_leaders_about_join_request(self):
+ # given
+ group_request = GroupRequest.objects.create(
+ user=self.user_2, group=self.group
+ )
+ # when
+ with mock.patch(MODULE_PATH + ".notify") as mock_notify:
+ group_request.notify_leaders()
+ # then
+ self.assertTrue(mock_notify.called)
+ _, kwargs = mock_notify.call_args
+ self.assertEqual(kwargs["user"],self.user_1)
+
+ @override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
+ def test_should_notify_leaders_about_leave_request(self):
+ # given
+ group_request = GroupRequest.objects.create(
+ user=self.user_2, group=self.group
+ )
+ # when
+ with mock.patch(MODULE_PATH + ".notify") as mock_notify:
+ group_request.notify_leaders()
+ # then
+ self.assertTrue(mock_notify.called)
+
+ @override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
+ def test_should_handle_notify_leaders_without_leaders(self):
+ # given
+ group = Group.objects.create(name='Dummy')
+ group.authgroup.internal = False
+ group.authgroup.hidden = False
+ group.authgroup.save()
+ group_request = GroupRequest.objects.create(
+ user=self.user_2, group=group
+ )
+ # when
+ with mock.patch(MODULE_PATH + ".notify") as mock_notify:
+ group_request.notify_leaders()
+ # then
+ self.assertFalse(mock_notify.called)
+
+ @override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=False)
+ def test_should_not_notify_leaders_if_disabled(self):
+ # given
+ group_request = GroupRequest.objects.create(
+ user=self.user_2, group=self.group
+ )
+ # when
+ with mock.patch(MODULE_PATH + ".notify") as mock_notify:
+ group_request.notify_leaders()
+ # then
+ self.assertFalse(mock_notify.called)
+
+ @override_settings(GROUPMANAGEMENT_REQUESTS_NOTIFICATION=True)
+ def test_should_notify_members_of_leader_groups_about_join_request(self):
+ # given
+ child_group = Group.objects.create(name='Child')
+ child_group.authgroup.internal = False
+ child_group.authgroup.hidden = False
+ child_group.authgroup.save()
+ child_group.authgroup.group_leader_groups.add(self.group)
+ group_request = GroupRequest.objects.create(
+ user=self.user_2, group=child_group
+ )
+ # when
+ with mock.patch(MODULE_PATH + ".notify") as mock_notify:
+ group_request.notify_leaders()
+ # then
+ self.assertTrue(mock_notify.called)
+ _, kwargs = mock_notify.call_args
+ self.assertEqual(kwargs["user"],self.user_1)
+
class TestRequestLog(TestCase):
-
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.group, cls.user_1, cls.user_2 = create_testdata()
+ cls.group, cls.user_1, cls.user_2, _ = create_testdata()
def test_requestor(self):
request_log = RequestLog.objects.create(
@@ -126,7 +195,7 @@ class TestRequestLog(TestCase):
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
- action = True
+ action=True
)
expected = 'Accept'
self.assertEqual(request_log.action_to_str(), expected)
@@ -136,7 +205,7 @@ class TestRequestLog(TestCase):
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
- action = False
+ action=False
)
expected = 'Reject'
self.assertEqual(request_log.action_to_str(), expected)
@@ -146,14 +215,13 @@ class TestRequestLog(TestCase):
group=self.group,
request_info='Clark Kent:Superheros',
request_actor=self.user_1,
- action = False
+ action=False
)
expected = self.user_2.profile.main_character
self.assertEqual(request_log.req_char(), expected)
class TestAuthGroup(TestCase):
-
def test_str(self):
group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group'
@@ -163,3 +231,56 @@ class TestAuthGroup(TestCase):
expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected)
+
+
+class TestAuthGroupRequestApprovers(TestCase):
+ def setUp(self) -> None:
+ self.group, self.user_1, self.user_2, self.user_3 = create_testdata()
+
+ def test_should_return_leaders_of_main_group_only(self):
+ # when
+ leaders = self.group.authgroup.group_request_approvers()
+ # then
+ self.assertSetEqual(leaders, {self.user_1})
+
+ def test_should_return_members_of_leading_groups_only(self):
+ # given
+ parent_group = Group.objects.create(name='Parent')
+ parent_group.authgroup.group_leaders.add(self.user_2)
+ self.user_1.groups.add(parent_group)
+ child_group = Group.objects.create(name='Child')
+ child_group.authgroup.internal = False
+ child_group.authgroup.hidden = False
+ child_group.authgroup.save()
+ child_group.authgroup.group_leader_groups.add(parent_group)
+ # when
+ leaders = child_group.authgroup.group_request_approvers()
+ # then
+ self.assertSetEqual(leaders, {self.user_1})
+
+ def test_should_return_leaders_of_main_group_and_members_of_leading_groups(self):
+ # given
+ parent_group = Group.objects.create(name='Parent')
+ parent_group.authgroup.group_leaders.add(self.user_2)
+ self.user_1.groups.add(parent_group)
+ child_group = Group.objects.create(name='Child')
+ child_group.authgroup.internal = False
+ child_group.authgroup.hidden = False
+ child_group.authgroup.save()
+ child_group.authgroup.group_leaders.add(self.user_3)
+ child_group.authgroup.group_leader_groups.add(self.group)
+ # when
+ leaders = child_group.authgroup.group_request_approvers()
+ # then
+ self.assertSetEqual(leaders, {self.user_1, self.user_3})
+
+ def test_can_handle_group_without_leaders(self):
+ # given
+ child_group = Group.objects.create(name='Child')
+ child_group.authgroup.internal = False
+ child_group.authgroup.hidden = False
+ child_group.authgroup.save()
+ # when
+ leaders = child_group.authgroup.group_request_approvers()
+ # then
+ self.assertSetEqual(leaders, set())
diff --git a/allianceauth/groupmanagement/views.py b/allianceauth/groupmanagement/views.py
index f351c288..994447dc 100755
--- a/allianceauth/groupmanagement/views.py
+++ b/allianceauth/groupmanagement/views.py
@@ -359,6 +359,7 @@ def group_request_add(request, group_id):
grouprequest.leave_request = False
grouprequest.save()
logger.info(f"Created group request for user {request.user} to group {Group.objects.get(id=group_id)}")
+ grouprequest.notify_leaders()
messages.success(request, _('Applied to group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups")
@@ -400,5 +401,6 @@ def group_request_leave(request, group_id):
grouprequest.leave_request = True
grouprequest.save()
logger.info(f"Created group leave request for user {request.user} to group {Group.objects.get(id=group_id)}")
+ grouprequest.notify_leaders()
messages.success(request, _('Applied to leave group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups")
diff --git a/docs/features/core/groupmanagement.md b/docs/features/core/groupmanagement.md
index 8abfeb2e..1f2685ea 100644
--- a/docs/features/core/groupmanagement.md
+++ b/docs/features/core/groupmanagement.md
@@ -35,6 +35,19 @@ Group leaders have the same abilities as users with the `group_management` permi
This allows you to more finely control who has access to manage which groups.
+## Settings
+
+Here is a list of available settings for Group Management. They can be configured by adding them to your AA settings file (``local.py``).
+Note that all settings are optional and the app will use the documented default settings if they are not used.
+
+```eval_rst
++---------------------------------------------+---------------------------------------------------------------------------+------------+
+| Name | Description | Default |
++=============================================+===========================================================================+============+
+| ``GROUPMANAGEMENT_REQUESTS_NOTIFICATION`` | Send Auth notifications to all group leaders for join and leave requests. | ``False`` |
++---------------------------------------------+---------------------------------------------------------------------------+------------+
+```
+
## Permissions
Group Management should be mostly done using group leaders, a series of permissions are included below for thoroughness.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index e65b4859..cbbe8ebd 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -4,12 +4,12 @@ sphinx_rtd_theme==0.5.0
recommonmark==0.6.0
# Autodoc dependencies
-django>=3.1.1,<4.0.0
+django>=3.2,<4.0.0
django-celery-beat>=2.0.0
django-bootstrap-form
django-sortedm2m
-django-esi>=1.5,<3.0
-celery>=4.3.0,<5.0.0,!=4.4.4
+django-esi>=3,<4
+celery>5,<6
celery_once
passlib
redis>=3.3.1,<4.0.0