Add option to notify approvers about new group requests

This commit is contained in:
Erik Kalkoken 2021-11-20 01:32:20 +00:00 committed by Ariel Rin
parent 2bd2c09c23
commit 982cac8c43
8 changed files with 411 additions and 160 deletions

View File

@ -14,8 +14,8 @@ class GroupManager:
@classmethod @classmethod
def get_joinable_groups_for_user( def get_joinable_groups_for_user(
cls, user: User, include_hidden = True cls, user: User, include_hidden=True
) -> QuerySet: ) -> QuerySet[Group]:
"""get groups a user could join incl. groups already joined""" """get groups a user could join incl. groups already joined"""
groups_qs = cls.get_joinable_groups(user.profile.state) groups_qs = cls.get_joinable_groups(user.profile.state)
@ -28,24 +28,27 @@ class GroupManager:
return groups_qs return groups_qs
@staticmethod @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""" """get groups that can be joined by user with given state"""
return Group.objects\ return (
.select_related('authgroup')\ Group.objects
.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')\
.exclude(authgroup__internal=True) .exclude(authgroup__internal=True)
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
)
@staticmethod @staticmethod
def get_group_leaders_groups(user: User): def get_all_non_internal_groups() -> QuerySet[Group]:
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \ """get groups that are not internal"""
Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all()) 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 @staticmethod
def joinable_group(group: Group, state: State) -> bool: def joinable_group(group: Group, state: State) -> bool:
@ -57,11 +60,11 @@ class GroupManager:
:param state: allianceauth.authentication.State object :param state: allianceauth.authentication.State object
:return: bool True if its joinable, False otherwise :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() and state not in group.authgroup.states.all()
): ):
return False return False
else:
return not group.authgroup.internal return not group.authgroup.internal
@staticmethod @staticmethod
@ -78,7 +81,7 @@ class GroupManager:
return user.has_perm('auth.group_management') return user.has_perm('auth.group_management')
@classmethod @classmethod
def can_manage_groups(cls, user:User ) -> bool: def can_manage_groups(cls, user:User) -> bool:
""" """
For use with user_passes_test decorator. For use with user_passes_test decorator.
Check if the user can manage groups. Either has the 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 :return: bool True if user can manage groups, False otherwise
""" """
if user.is_authenticated: 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 return False
@classmethod @classmethod
@ -100,19 +106,19 @@ class GroupManager:
:return: True if the user can manage the group :return: True if the user can manage the group
""" """
if user.is_authenticated: 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 return False
@classmethod @classmethod
def pending_requests_count_for_user(cls, user: User) -> int: def pending_requests_count_for_user(cls, user: User) -> int:
"""Returns the number of pending group requests for the given user""" """Returns the number of pending group requests for the given user"""
if cls.has_management_permission(user): if cls.has_management_permission(user):
return GroupRequest.objects.all().count() return GroupRequest.objects.all().count()
else:
return ( return (
GroupRequest.objects GroupRequest.objects
.filter(group__authgroup__group_leaders__exact=user) .filter(group__in=list(cls.get_group_leaders_groups(user)))
.select_related("group__authgroup__group_leaders")
.count() .count()
) )

View File

@ -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 <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', 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 <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', 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.<br>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.<br>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.<br>', related_name='valid_states', to='authentication.State'),
),
]

View File

@ -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 Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from allianceauth.authentication.models import State from allianceauth.authentication.models import State
from allianceauth.notifications import notify
class GroupRequest(models.Model): class GroupRequest(models.Model):
"""Request from a user for joining or leaving a group."""
leave_request = models.BooleanField(default=0) leave_request = models.BooleanField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self):
return self.user.username + ":" + self.group.name
@property @property
def main_char(self): def main_char(self):
""" """
@ -19,11 +29,22 @@ class GroupRequest(models.Model):
""" """
return self.user.profile.main_character return self.user.profile.main_character
def __str__(self): def notify_leaders(self) -> None:
return self.user.username + ":" + self.group.name """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): class RequestLog(models.Model):
"""Log entry about who joined and left a group and who approved it."""
request_type = models.BooleanField(null=True) request_type = models.BooleanField(null=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
request_info = models.CharField(max_length=254) request_info = models.CharField(max_length=254)
@ -61,11 +82,12 @@ class AuthGroup(models.Model):
e.g. group.authgroup.internal e.g. group.authgroup.internal
Logic: Logic:
Internal - not requestable by users, at all. Covers Corp_, Alliance_, Members etc groups. Internal - not requestable by users, at all. Covers Corp_, Alliance_,
Groups are internal by default 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 Public - Other options are respected, but any user will be able to become
have no API etc entered. Auth will not manage these groups automatically so user removal is up to 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. group managers/leaders.
Not Internal and: 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 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) group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
internal = models.BooleanField(
internal = models.BooleanField(default=True, default=True,
help_text="Internal group, users cannot see, join or request to join this group.<br>" help_text=_(
"Internal group, users cannot see, join or request to join this group.<br>"
"Used for groups such as Members, Corp_*, Alliance_* etc.<br>" "Used for groups such as Members, Corp_*, Alliance_* etc.<br>"
"<b>Overrides Hidden and Open options when selected.</b>") "<b>Overrides Hidden and Open options when selected.</b>"
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. <br>" hidden = models.BooleanField(
"If the group is not open users will need their request manually approved.") default=True,
public = models.BooleanField(default=False, help_text=_(
help_text="Group is public. Any registered user is able to join this group, with " "Group is hidden from users but can still join with the correct link."
"visibility based on the other options set for this group.<br> Auth will " )
"not remove users from this group automatically when they are no longer " )
"authenticated.") open = models.BooleanField(
# Group leaders have management access to this group default=False,
group_leaders = models.ManyToManyField(User, related_name='leads_groups', blank=True, help_text=_(
help_text="Group leaders can process group requests for this group " "Group is open and users will be automatically added upon request.<br>"
"specifically. Use the auth.group_management permission to allow " "If the group is not open users will need their request manually approved."
"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, public = models.BooleanField(
help_text="Group leaders can process group requests for this group " default=False,
"specifically. Use the auth.group_management permission to allow " help_text=_(
"a user to manage all groups.") "Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br>"
"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 <code>auth.group_management</code> permission to allow "
"a user to manage all groups.<br>"
)
)
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 <code>auth.group_management</code> permission "
"to allow a user to manage all groups.<br>")
)
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.<br>"
)
)
description = models.TextField(
max_length=512,
blank=True,
help_text=_(
"Short description <i>(max. 512 characters)</i> "
"of the group shown to users."
)
)
states = models.ManyToManyField(State, related_name='valid_states', blank=True, class Meta:
help_text="States listed here will have the ability to join this group provided " permissions = (
"they have the proper permissions.") ("request_groups", _("Can request non-public groups")),
)
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)" default_permissions = tuple()
"</i> of the group shown to users.")
def __str__(self): def __str__(self):
return self.group.name return self.group.name
class Meta: def group_request_approvers(self) -> Set[User]:
permissions = ( """Return all users who can approve a group request."""
("request_groups", "Can request non-public groups"), 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) @receiver(post_save, sender=Group)

View File

@ -1,8 +1,5 @@
from unittest.mock import Mock, patch
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
@ -44,9 +41,9 @@ class GroupManagementVisibilityTestCase(TestCase):
self._refresh_user() self._refresh_user()
groups = GroupManager.get_group_leaders_groups(self.user) groups = GroupManager.get_group_leaders_groups(self.user)
self.assertIn(self.group1, groups) #avail due to user self.assertIn(self.group1, groups) # avail due to user
self.assertNotIn(self.group2, groups) #not avail due to group self.assertNotIn(self.group2, groups) # not avail due to group
self.assertNotIn(self.group3, groups) #not avail at all self.assertNotIn(self.group3, groups) # not avail at all
self.user.groups.add(self.group1) self.user.groups.add(self.group1)
self._refresh_user() self._refresh_user()
@ -71,70 +68,66 @@ class GroupManagementVisibilityTestCase(TestCase):
class TestGroupManager(TestCase): class TestGroupManager(TestCase):
def setUp(self) -> None:
@classmethod
def setUpClass(cls):
super().setUpClass()
# group 1 # group 1
cls.group_default = Group.objects.create(name='default') self.group_default = Group.objects.create(name='default')
cls.group_default.authgroup.description = 'Default Group' self.group_default.authgroup.description = 'Default Group'
cls.group_default.authgroup.internal = False self.group_default.authgroup.internal = False
cls.group_default.authgroup.hidden = False self.group_default.authgroup.hidden = False
cls.group_default.authgroup.save() self.group_default.authgroup.save()
# group 2 # group 2
cls.group_internal = Group.objects.create(name='internal') self.group_internal = Group.objects.create(name='internal')
cls.group_internal.authgroup.description = 'Internal Group' self.group_internal.authgroup.description = 'Internal Group'
cls.group_internal.authgroup.internal = True self.group_internal.authgroup.internal = True
cls.group_internal.authgroup.save() self.group_internal.authgroup.save()
# group 3 # group 3
cls.group_hidden = Group.objects.create(name='hidden') self.group_hidden = Group.objects.create(name='hidden')
cls.group_hidden.authgroup.description = 'Hidden Group' self.group_hidden.authgroup.description = 'Hidden Group'
cls.group_hidden.authgroup.internal = False self.group_hidden.authgroup.internal = False
cls.group_hidden.authgroup.hidden = True self.group_hidden.authgroup.hidden = True
cls.group_hidden.authgroup.save() self.group_hidden.authgroup.save()
# group 4 # group 4
cls.group_open = Group.objects.create(name='open') self.group_open = Group.objects.create(name='open')
cls.group_open.authgroup.description = 'Open Group' self.group_open.authgroup.description = 'Open Group'
cls.group_open.authgroup.internal = False self.group_open.authgroup.internal = False
cls.group_open.authgroup.hidden = False self.group_open.authgroup.hidden = False
cls.group_open.authgroup.open = True self.group_open.authgroup.open = True
cls.group_open.authgroup.save() self.group_open.authgroup.save()
# group 5 # group 5
cls.group_public_1 = Group.objects.create(name='public 1') self.group_public_1 = Group.objects.create(name='public 1')
cls.group_public_1.authgroup.description = 'Public Group 1' self.group_public_1.authgroup.description = 'Public Group 1'
cls.group_public_1.authgroup.internal = False self.group_public_1.authgroup.internal = False
cls.group_public_1.authgroup.hidden = False self.group_public_1.authgroup.hidden = False
cls.group_public_1.authgroup.public = True self.group_public_1.authgroup.public = True
cls.group_public_1.authgroup.save() self.group_public_1.authgroup.save()
# group 6 # group 6
cls.group_public_2 = Group.objects.create(name='public 2') self.group_public_2 = Group.objects.create(name='public 2')
cls.group_public_2.authgroup.description = 'Public Group 2' self.group_public_2.authgroup.description = 'Public Group 2'
cls.group_public_2.authgroup.internal = False self.group_public_2.authgroup.internal = False
cls.group_public_2.authgroup.hidden = True self.group_public_2.authgroup.hidden = True
cls.group_public_2.authgroup.open = True self.group_public_2.authgroup.open = True
cls.group_public_2.authgroup.public = True self.group_public_2.authgroup.public = True
cls.group_public_2.authgroup.save() self.group_public_2.authgroup.save()
# group 7 # group 7
cls.group_default_member = Group.objects.create(name='default members') self.group_default_member = Group.objects.create(name='default members')
cls.group_default_member.authgroup.description = \ self.group_default_member.authgroup.description = \
'Default Group for members only' 'Default Group for members only'
cls.group_default_member.authgroup.internal = False self.group_default_member.authgroup.internal = False
cls.group_default_member.authgroup.hidden = False self.group_default_member.authgroup.hidden = False
cls.group_default_member.authgroup.open = False self.group_default_member.authgroup.open = False
cls.group_default_member.authgroup.public = False self.group_default_member.authgroup.public = False
cls.group_default_member.authgroup.states.add( self.group_default_member.authgroup.states.add(
AuthUtils.get_member_state() 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') self.user = AuthUtils.create_user('Bruce Wayne')
def test_get_joinable_group_member(self): 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): def test_get_joinable_groups_for_user_member_w_permission(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True) 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 'groupmanagement.request_groups', self.user
) )
result = GroupManager.get_joinable_groups_for_user(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): def test_get_joinable_groups_for_user_member_w_permission_no_hidden(self):
AuthUtils.assign_state(self.user, AuthUtils.get_member_state(), True) 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 'groupmanagement.request_groups', self.user
) )
result = GroupManager.get_joinable_groups_for_user( result = GroupManager.get_joinable_groups_for_user(
@ -273,7 +266,7 @@ class TestGroupManager(TestCase):
def test_has_management_permission(self): def test_has_management_permission(self):
user = AuthUtils.create_user('Clark Kent') 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 'auth.group_management', user
) )
self.assertTrue(GroupManager.has_management_permission(user)) self.assertTrue(GroupManager.has_management_permission(user))
@ -288,7 +281,7 @@ class TestGroupManager(TestCase):
def test_can_manage_groups_has_perm(self): def test_can_manage_groups_has_perm(self):
user = AuthUtils.create_user('Clark Kent') 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 'auth.group_management', user
) )
self.assertTrue(GroupManager.can_manage_groups(user)) self.assertTrue(GroupManager.can_manage_groups(user))
@ -306,7 +299,7 @@ class TestGroupManager(TestCase):
def test_can_manage_group_has_perm(self): def test_can_manage_group_has_perm(self):
user = AuthUtils.create_user('Clark Kent') 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 'auth.group_management', user
) )
self.assertTrue( self.assertTrue(
@ -433,11 +426,21 @@ class TestPendingRequestsCountForUser(TestCase):
# when user_requestor is requesting access to group 1 # when user_requestor is requesting access to group 1
# then return 1 for user_leader_4 # then return 1 for user_leader_4
user_leader_4 = AuthUtils.create_member("Lex Luther") user_leader_4 = AuthUtils.create_member("Lex Luther")
AuthUtils.add_permission_to_user_by_name("auth.group_management", user_leader_4) user_leader_4 = AuthUtils.add_permission_to_user_by_name(
user_leader_4 = User.objects.get(pk=user_leader_4.pk) "auth.group_management", user_leader_4
GroupRequest.objects.create(
user=self.user_requestor, group=self.group_1
) )
GroupRequest.objects.create(user=self.user_requestor, group=self.group_1)
self.assertEqual( self.assertEqual(
GroupManager.pending_requests_count_for_user(self.user_leader_1), 1 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)

View File

@ -1,31 +1,22 @@
from unittest import mock from unittest import mock
from django.contrib.auth.models import User, Group from django.contrib.auth.models import Group
from django.test import TestCase from django.test import TestCase, override_settings
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import (
EveCorporationInfo, EveAllianceInfo, EveCharacter
)
from ..models import GroupRequest, RequestLog from ..models import GroupRequest, RequestLog
MODULE_PATH = "allianceauth.groupmanagement.models"
def create_testdata(): 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 1
group = Group.objects.create(name='Superheros') group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group' group.authgroup.description = 'Default Group'
group.authgroup.internal = False group.authgroup.internal = False
group.authgroup.hidden = False group.authgroup.hidden = False
group.authgroup.save() group.authgroup.save()
# user 1 # user 1
user_1 = AuthUtils.create_user('Bruce Wayne') user_1 = AuthUtils.create_user('Bruce Wayne')
AuthUtils.add_main_character_2( AuthUtils.add_main_character_2(
@ -37,7 +28,6 @@ def create_testdata():
) )
user_1.groups.add(group) user_1.groups.add(group)
group.authgroup.group_leaders.add(user_1) group.authgroup.group_leaders.add(user_1)
# user 2 # user 2
user_2 = AuthUtils.create_user('Clark Kent') user_2 = AuthUtils.create_user('Clark Kent')
AuthUtils.add_main_character_2( AuthUtils.add_main_character_2(
@ -45,18 +35,25 @@ def create_testdata():
name='Clark Kent', name='Clark Kent',
character_id=1002, character_id=1002,
corp_id=2002, 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): class TestGroupRequest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() 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): def test_main_char(self):
group_request = GroupRequest.objects.create( group_request = GroupRequest.objects.create(
@ -74,13 +71,85 @@ class TestGroupRequest(TestCase):
expected = 'Bruce Wayne:Superheros' expected = 'Bruce Wayne:Superheros'
self.assertEqual(str(group_request), expected) 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): class TestRequestLog(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() 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): def test_requestor(self):
request_log = RequestLog.objects.create( request_log = RequestLog.objects.create(
@ -126,7 +195,7 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = True action=True
) )
expected = 'Accept' expected = 'Accept'
self.assertEqual(request_log.action_to_str(), expected) self.assertEqual(request_log.action_to_str(), expected)
@ -136,7 +205,7 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = False action=False
) )
expected = 'Reject' expected = 'Reject'
self.assertEqual(request_log.action_to_str(), expected) self.assertEqual(request_log.action_to_str(), expected)
@ -146,14 +215,13 @@ class TestRequestLog(TestCase):
group=self.group, group=self.group,
request_info='Clark Kent:Superheros', request_info='Clark Kent:Superheros',
request_actor=self.user_1, request_actor=self.user_1,
action = False action=False
) )
expected = self.user_2.profile.main_character expected = self.user_2.profile.main_character
self.assertEqual(request_log.req_char(), expected) self.assertEqual(request_log.req_char(), expected)
class TestAuthGroup(TestCase): class TestAuthGroup(TestCase):
def test_str(self): def test_str(self):
group = Group.objects.create(name='Superheros') group = Group.objects.create(name='Superheros')
group.authgroup.description = 'Default Group' group.authgroup.description = 'Default Group'
@ -163,3 +231,56 @@ class TestAuthGroup(TestCase):
expected = 'Superheros' expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected) 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())

View File

@ -359,6 +359,7 @@ def group_request_add(request, group_id):
grouprequest.leave_request = False grouprequest.leave_request = False
grouprequest.save() grouprequest.save()
logger.info(f"Created group request for user {request.user} to group {Group.objects.get(id=group_id)}") 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}) messages.success(request, _('Applied to group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")
@ -400,5 +401,6 @@ def group_request_leave(request, group_id):
grouprequest.leave_request = True grouprequest.leave_request = True
grouprequest.save() grouprequest.save()
logger.info(f"Created group leave request for user {request.user} to group {Group.objects.get(id=group_id)}") 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}) messages.success(request, _('Applied to leave group %(group)s.') % {"group": group})
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")

View File

@ -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. 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 ## Permissions
Group Management should be mostly done using group leaders, a series of permissions are included below for thoroughness. Group Management should be mostly done using group leaders, a series of permissions are included below for thoroughness.

View File

@ -4,12 +4,12 @@ sphinx_rtd_theme==0.5.0
recommonmark==0.6.0 recommonmark==0.6.0
# Autodoc dependencies # Autodoc dependencies
django>=3.1.1,<4.0.0 django>=3.2,<4.0.0
django-celery-beat>=2.0.0 django-celery-beat>=2.0.0
django-bootstrap-form django-bootstrap-form
django-sortedm2m django-sortedm2m
django-esi>=1.5,<3.0 django-esi>=3,<4
celery>=4.3.0,<5.0.0,!=4.4.4 celery>5,<6
celery_once celery_once
passlib passlib
redis>=3.3.1,<4.0.0 redis>=3.3.1,<4.0.0